refactor: one definition per naming rule, drop ansible (step 5)
Consolidates the last duplicated derivations, and removes the ansible
tree along with the CLI surface that existed only to drive it.
Naming rules:
* agebd/names.py holds ServiceName -- the single definition of how a
service name is spelled in each context: dir_name, module_name,
ioc_name, systemd_unit_name and the case forms. The environment
suffix is deliberately not part of it; that stays a deploy-time
token applied to these base names.
* cli's Service model delegates every name_* property to it, so the
CLI and the CI scripts derive names the same way.
* scripts/ci/service_names.py emits the derived forms as shell
assignments, and deploy-and-restart-service.sh evals them instead of
carrying its own tr calls and its own copy of the systemd naming
convention. Values are shell-quoted, and an invalid name exits
non-zero emitting nothing, so an eval cannot leave empty variables
behind.
Registry lookups:
* agebd.subs.find_service_entry is now the only place that answers
"which registry entry is this service", used by read_ioc_port,
get_ioc_port.py and cli's ServiceRegistry. get_ioc_port.py no longer
parses the registry itself.
* That also settles a semantic split: ServiceRegistry matched names
case-sensitively while read_ioc_port did not. Both now match
case-insensitively via ServiceName, so 'tune', 'Tune' and 'TUNE'
resolve to the same service everywhere.
Ansible removal:
* ansible/ is deleted, along with cli/src/ioc.py, cli/src/core/
ansible.py, the IOC_ENV enum, the ansible and ansible-runner
dependencies, and docs/developer/ansible.md.
* This removes the 'agebd ioc install|start|restart' commands. They
were pure playbook wrappers, so they could not outlive the
playbooks; IOC install, start and restart now have no automation
path in this repo.
bin/start_service.sh keeps deriving its two name forms in bash, with a
comment saying why: it runs on the deploy host before the service venv
exists, so it cannot import agebd. It is the one deliberate exception.
Verified: get_ioc_port.py, cli's ServiceRegistry and agebd's renderer all
return 50007 for 'tune', 'Tune' and 'TUNE'; the shell-emitted names match
the committed on-disk layout for real services including the dashed
OpticsFF-X02S; the CLI runs with only the 'service' command left.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ee5f331f4c
commit
4a73e52a35
37 files changed
+393
-788
No files matched your search
+22
-17
@@ -1,24 +1,29 @@
|
||||
"""
|
||||
Print a service's IOC port.
|
||||
|
||||
get_ioc_port.py tune
|
||||
|
||||
The lookup itself lives in agebd.subs, which is also what render_subs.py and
|
||||
the CLI use, so there is only one answer to "which port is this service on".
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
from agebd.utils import get_git_root
|
||||
|
||||
REPO_ROOT = get_git_root(".")
|
||||
SERVICES_REGISTRY_FILE = REPO_ROOT / "config" / "services_registry.yml"
|
||||
from agebd.subs import (
|
||||
SERVICES_REGISTRY_FILENAME,
|
||||
SubstitutionError,
|
||||
read_ioc_port,
|
||||
resolve_config_dir,
|
||||
)
|
||||
|
||||
|
||||
def get_ioc_port(service_name_lower: str):
|
||||
with open(SERVICES_REGISTRY_FILE) as f:
|
||||
d = yaml.safe_load(f)
|
||||
|
||||
for service_dict in d["services"]:
|
||||
if service_dict["name"].lower() == service_name_lower:
|
||||
return service_dict["ioc_port"]
|
||||
|
||||
raise ValueError(f"Service with lowercase name {service_name_lower} not found")
|
||||
def get_ioc_port(service_name: str) -> int:
|
||||
return read_ioc_port(resolve_config_dir() / SERVICES_REGISTRY_FILENAME, service_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
service_name_lower = sys.argv[1]
|
||||
port = get_ioc_port(service_name_lower)
|
||||
print(port)
|
||||
try:
|
||||
print(get_ioc_port(sys.argv[1]))
|
||||
except SubstitutionError as e:
|
||||
print(f"error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Emit a service's derived names as shell assignments.
|
||||
|
||||
eval "$(service_names.py TauBPM)"
|
||||
echo "$SYSTEMD_UNIT_NAME" # AGEBD-SERVICE-TAUBPM.service
|
||||
|
||||
Every form comes from agebd.names.ServiceName, so the deploy scripts carry no
|
||||
copy of the naming conventions. Values are shell-quoted; an invalid service
|
||||
name exits non-zero and emits nothing, so an unguarded eval cannot silently
|
||||
leave the caller with empty variables.
|
||||
"""
|
||||
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
from agebd.names import InvalidServiceName, ServiceName
|
||||
|
||||
#: Shell variable name -> ServiceName attribute.
|
||||
EXPORTED = {
|
||||
"SERVICE_NAME_CAMEL": "camel",
|
||||
"SERVICE_NAME_UPPER": "upper",
|
||||
"SERVICE_NAME_LOWER": "lower",
|
||||
"SERVICE_DIR_NAME": "dir_name",
|
||||
"SERVICE_MODULE_NAME": "module_name",
|
||||
"IOC_NAME": "ioc_name",
|
||||
"SYSTEMD_UNIT_NAME": "systemd_unit_name",
|
||||
}
|
||||
|
||||
|
||||
def shell_assignments(name: str) -> str:
|
||||
service = ServiceName(name)
|
||||
return "\n".join(
|
||||
f"{var}={shlex.quote(getattr(service, attr))}" for var, attr in EXPORTED.items()
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print(f"usage: {sys.argv[0]} <service-name>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
try:
|
||||
print(shell_assignments(sys.argv[1]))
|
||||
except InvalidServiceName as e:
|
||||
print(f"error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,72 @@
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from agebd.names import InvalidServiceName
|
||||
from agebd.utils import get_git_root
|
||||
|
||||
from ci.service_names import EXPORTED, shell_assignments
|
||||
|
||||
SCRIPT = get_git_root(__file__) / "scripts" / "ci" / "service_names.py"
|
||||
|
||||
|
||||
def run_in_shell(name):
|
||||
"""Eval the output in a real shell and read the variables back."""
|
||||
assignments = shell_assignments(name)
|
||||
readback = "\n".join(f'printf "%s=%s\\n" {var} "${var}"' for var in EXPORTED)
|
||||
out = subprocess.run(
|
||||
["bash", "-c", f"{assignments}\n{readback}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout
|
||||
return dict(line.split("=", 1) for line in out.strip().splitlines())
|
||||
|
||||
|
||||
def test_emits_every_derived_form():
|
||||
values = run_in_shell("TauBPM")
|
||||
|
||||
assert values == {
|
||||
"SERVICE_NAME_CAMEL": "TauBPM",
|
||||
"SERVICE_NAME_UPPER": "TAUBPM",
|
||||
"SERVICE_NAME_LOWER": "taubpm",
|
||||
"SERVICE_DIR_NAME": "taubpm",
|
||||
"SERVICE_MODULE_NAME": "agebd_taubpm",
|
||||
"IOC_NAME": "AGEBD-CPCL-TAUBPM",
|
||||
"SYSTEMD_UNIT_NAME": "AGEBD-SERVICE-TAUBPM.service",
|
||||
}
|
||||
|
||||
|
||||
def test_dashed_name_survives_the_shell():
|
||||
values = run_in_shell("OpticsFF-X02S")
|
||||
|
||||
assert values["SERVICE_DIR_NAME"] == "opticsff-x02s"
|
||||
assert values["SERVICE_MODULE_NAME"] == "agebd_opticsff_x02s"
|
||||
assert values["IOC_NAME"] == "AGEBD-CPCL-OPTICSFF-X02S"
|
||||
|
||||
|
||||
def test_invalid_name_raises():
|
||||
with pytest.raises(InvalidServiceName):
|
||||
shell_assignments("Tune; rm -rf /")
|
||||
|
||||
|
||||
def test_cli_exits_non_zero_and_emits_nothing_on_a_bad_name():
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "Tune Bump"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert result.stdout == ""
|
||||
assert "Invalid service name" in result.stderr
|
||||
|
||||
|
||||
def test_cli_requires_exactly_one_argument():
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(SCRIPT)], capture_output=True, text=True, check=False
|
||||
)
|
||||
|
||||
assert result.returncode == 2
|
||||
assert "usage:" in result.stderr
|
||||
Reference in new issue
Block a user