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:
co-authored by
Claude Opus 5
parent
ee5f331f4c
commit
4a73e52a35
@@ -7,17 +7,22 @@ echo "=========================================="
|
||||
echo "Deploying service: $SERVICE_NAME (${AGEBD_ENV})"
|
||||
echo "=========================================="
|
||||
|
||||
SERVICE_NAME_LOWER=$(echo "$SERVICE_NAME" | tr '[:upper:]' '[:lower:]')
|
||||
SERVICE_NAME_UPPER=$(echo "$SERVICE_NAME" | tr '[:lower:]' '[:upper:]')
|
||||
SERVICE_DIR=/sls/bd/hla/${AGEBD_ENV}/services/${SERVICE_NAME_LOWER}/current
|
||||
SYSTEMD_UNIT_NAME=AGEBD-SERVICE-${SERVICE_NAME_UPPER}.service
|
||||
# Name forms and the systemd naming convention come from
|
||||
# agebd.names.ServiceName, so this script carries no copy of them.
|
||||
if ! SERVICE_NAMES=$(uv run --directory scripts ci/service_names.py "${SERVICE_NAME}"); then
|
||||
echo "Could not derive names for '${SERVICE_NAME}' -- aborting deploy" >&2
|
||||
exit 1
|
||||
fi
|
||||
eval "${SERVICE_NAMES}"
|
||||
|
||||
SERVICE_DIR=/sls/bd/hla/${AGEBD_ENV}/services/${SERVICE_DIR_NAME}/current
|
||||
SYSTEMD_UNIT_FILE_PATH=${SERVICE_DIR}/systemd/${SYSTEMD_UNIT_NAME}
|
||||
IOC_DIR=${SERVICE_DIR}/ioc
|
||||
|
||||
# TODO: see https://gitea.psi.ch/sls/hla_framework_bd/issues/50
|
||||
# rm -rf "${SERVICE_DIR}"
|
||||
# mkdir -p ${SERVICE_DIR}
|
||||
# cp -r ./services/${SERVICE_NAME_LOWER}/current/* ${SERVICE_DIR}/
|
||||
# cp -r ./services/${SERVICE_DIR_NAME}/current/* ${SERVICE_DIR}/
|
||||
|
||||
rsync -avz --delete \
|
||||
--exclude='.nfs*' \
|
||||
@@ -26,7 +31,7 @@ rsync -avz --delete \
|
||||
--exclude='__pycache__/' \
|
||||
--exclude='*.pyc' \
|
||||
--exclude='.pytest_cache/' \
|
||||
./services/${SERVICE_NAME_LOWER}/current/* ${SERVICE_DIR}/
|
||||
./services/${SERVICE_DIR_NAME}/current/* ${SERVICE_DIR}/
|
||||
|
||||
### SUBSTITUTIONS
|
||||
# Every @@token@@ is resolved from config/environments.yml and
|
||||
@@ -34,7 +39,7 @@ rsync -avz --delete \
|
||||
# deploy instead of leaving the placeholder in a file the IOC will read.
|
||||
if ! uv run --directory scripts ci/render_subs.py \
|
||||
--env "${AGEBD_ENV}" \
|
||||
--service "${SERVICE_NAME_LOWER}" \
|
||||
--service "${SERVICE_DIR_NAME}" \
|
||||
"${IOC_DIR}" "${SERVICE_DIR}/systemd"; then
|
||||
echo "Substitution failed -- aborting deploy of ${SERVICE_NAME}" >&2
|
||||
exit 1
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
[defaults]
|
||||
remote_tmp = /tmp/ansible-$USER
|
||||
|
||||
# some responses of 'ioc ...' cli are non-utf8
|
||||
module_strict_utf8_response = False
|
||||
|
||||
# ./ refers to dir in which this file is located
|
||||
collections_path = ./installed-collections
|
||||
@@ -1,3 +0,0 @@
|
||||
collections:
|
||||
- name: ansible.posix
|
||||
version: 1.6.0
|
||||
@@ -1,19 +0,0 @@
|
||||
all:
|
||||
vars:
|
||||
# Suffix values for this environment live in config/environments.yml --
|
||||
# do not reintroduce them here.
|
||||
agebd_env: "dev"
|
||||
|
||||
children:
|
||||
lc:
|
||||
hosts:
|
||||
sls-lc:
|
||||
ansible_remote_tmp: /tmp/.ansible_${ansible_user}_tmp
|
||||
vserv:
|
||||
hosts:
|
||||
sls-vserv-bd-01-dev:
|
||||
ansible_ssh_pipelining: true
|
||||
ansible_ssh_common_args: '-o ForwardAgent=yes'
|
||||
vserv-hla:
|
||||
hosts:
|
||||
sls-vserv-bd-hla01-dev:
|
||||
@@ -1,18 +0,0 @@
|
||||
all:
|
||||
vars:
|
||||
# Suffix values for this environment live in config/environments.yml --
|
||||
# do not reintroduce them here.
|
||||
agebd_env: "prod"
|
||||
|
||||
children:
|
||||
lc:
|
||||
hosts:
|
||||
sls-lca:
|
||||
ansible_remote_tmp: /tmp/.ansible_${ansible_user}_tmp
|
||||
vserv:
|
||||
hosts:
|
||||
sls-vserv-bd-01:
|
||||
ansible_ssh_pipelining: true
|
||||
vserv-hla:
|
||||
hosts:
|
||||
sls-vserv-bd-hla01:
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
- name: Add a new service
|
||||
hosts: vserv-hla
|
||||
connection: local
|
||||
gather_facts: true # Gather once here, available to all included tasks!
|
||||
|
||||
vars:
|
||||
repo_root: "{{ playbook_dir }}/../.."
|
||||
|
||||
tasks:
|
||||
# - name: Install Master IOC
|
||||
# vars:
|
||||
# preferred_service_name_lower: master
|
||||
# branch_name: "feature/add-service-{{ service_name_lower }}"
|
||||
# import_tasks: ../tasks/ioc/ioc-install.yml
|
||||
|
||||
# - name: Restart Master IOC
|
||||
# vars:
|
||||
# preferred_service_name_lower: master
|
||||
# branch_name: "feature/add-service-{{ service_name_lower }}"
|
||||
# import_tasks: ../tasks/ioc/ioc-restart.yml
|
||||
|
||||
- name: Deploy and Restart Master Service
|
||||
vars:
|
||||
preferred_service_name_lower: master
|
||||
branch_name: "feature/add-service-{{ service_name_lower }}"
|
||||
import_tasks: ../tasks/service/service-deploy-and-restart.yml
|
||||
|
||||
# - name: Install New IOC
|
||||
# vars:
|
||||
# branch_name: "feature/add-service-{{ service_name_lower }}"
|
||||
# import_tasks: ../tasks/ioc/ioc-install.yml
|
||||
|
||||
# - name: Start New IOC
|
||||
# vars:
|
||||
# branch_name: "feature/add-service-{{ service_name_lower }}"
|
||||
# import_tasks: ../tasks/ioc/ioc-start.yml
|
||||
|
||||
- name: Deploy and Restart New Service
|
||||
vars:
|
||||
branch_name: "feature/add-service-{{ service_name_lower }}"
|
||||
import_tasks: ../tasks/service/service-deploy-and-restart.yml
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
- name: Deploy and Restart Modified Services
|
||||
hosts: vserv-hla
|
||||
connection: local
|
||||
|
||||
vars:
|
||||
# Passed from the Gitea workflow, e.g., "master playground"
|
||||
changed_services_raw: ""
|
||||
agebd_env: "dev"
|
||||
repo_root: "{{ playbook_dir }}/../.."
|
||||
|
||||
tasks:
|
||||
# Fail early if no services were modified
|
||||
- name: Check if there are services to deploy
|
||||
ansible.builtin.meta: end_play
|
||||
when: changed_services_raw | trim == ""
|
||||
|
||||
# Main Loop: loop through the list of modified service names
|
||||
- name: Deploy and Restart Modified Services
|
||||
include_tasks: ../tasks/service/service-deploy-and-restart.yml
|
||||
loop: "{{ changed_services_raw.split() }}"
|
||||
loop_control:
|
||||
loop_var: service_name_lower
|
||||
extended: yes
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
- name: Install an IOC
|
||||
hosts: lc
|
||||
gather_facts: false
|
||||
|
||||
vars:
|
||||
workspace_dir: "/tmp/hla_framework_bd"
|
||||
# Controller-side checkout, where config/environments.yml is read from
|
||||
repo_root: "{{ playbook_dir }}/../.."
|
||||
|
||||
tasks:
|
||||
- name: Run ioc install tasks
|
||||
ansible.builtin.import_tasks: ../tasks/ioc/ioc-install.yml
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
- name: Restart an IOC
|
||||
hosts: lc
|
||||
gather_facts: false
|
||||
|
||||
vars:
|
||||
workspace_dir: "/tmp/hla_framework_bd"
|
||||
# Controller-side checkout, where config/environments.yml is read from
|
||||
repo_root: "{{ playbook_dir }}/../.."
|
||||
|
||||
tasks:
|
||||
- name: Run ioc restart tasks
|
||||
ansible.builtin.include_tasks: ../tasks/ioc/ioc-restart.yml
|
||||
@@ -1,16 +0,0 @@
|
||||
# TODO: ansible seems to be very slow on this machine
|
||||
# each step takes quite a while (e.g. cloning repo, which
|
||||
# is significantly faster on other machines).
|
||||
---
|
||||
- name: Start an IOC
|
||||
hosts: vserv
|
||||
gather_facts: false
|
||||
|
||||
vars:
|
||||
workspace_dir: "/tmp/hla_framework_bd"
|
||||
# Controller-side checkout, where config/environments.yml is read from
|
||||
repo_root: "{{ playbook_dir }}/../.."
|
||||
|
||||
tasks:
|
||||
- name: Run ioc start tasks
|
||||
ansible.builtin.import_tasks: ../tasks/ioc/ioc-start.yml
|
||||
@@ -1,66 +0,0 @@
|
||||
---
|
||||
- name: Init service name
|
||||
ansible.builtin.import_tasks: ../utils/init-service-name.yml
|
||||
|
||||
- name: Clone repo
|
||||
ansible.builtin.import_tasks: ../utils/clone-repo.yml
|
||||
|
||||
- name: Get and set ioc_port
|
||||
ansible.builtin.import_tasks: ../utils/get-ioc-port.yml
|
||||
|
||||
- name: Load deploy-time substitutions
|
||||
ansible.builtin.import_tasks: ../utils/load-substitutions.yml
|
||||
|
||||
- name: "[{{ target_service_name_lower }}] Establish environment naming conventions"
|
||||
ansible.builtin.set_fact:
|
||||
service_name_upper: "{{ target_service_name_lower | upper }}"
|
||||
|
||||
- name: "[{{ target_service_name_lower }}] Define file paths"
|
||||
ansible.builtin.set_fact:
|
||||
ioc_base_dir: "{{ workspace_dir }}/services/{{ target_service_name_lower }}/current/ioc"
|
||||
orig_subs_name: "AGEBD-CPCL-{{ service_name_upper }}_main.subs"
|
||||
target_subs_name: "AGEBD-CPCL-{{ service_name_upper }}{{ agebd_env_suffix_upper }}_main.subs"
|
||||
orig_params_name: "AGEBD-CPCL-{{ service_name_upper }}_parameters.yaml"
|
||||
target_params_name: "AGEBD-CPCL-{{ service_name_upper }}{{ agebd_env_suffix_upper }}_parameters.yaml"
|
||||
|
||||
# Rename the subs file if we are in DEV
|
||||
- name: "[{{ target_service_name_lower }}] Rename substitution file for environment"
|
||||
ansible.builtin.command:
|
||||
cmd: "mv {{ ioc_base_dir }}/{{ orig_subs_name }} {{ ioc_base_dir }}/{{ target_subs_name }}"
|
||||
removes: "{{ ioc_base_dir }}/{{ orig_subs_name }}" # Only runs if the original file exists
|
||||
|
||||
# Rename the paramters file if we are in DEV
|
||||
- name: "[{{ target_service_name_lower }}] Rename parameters file for environment"
|
||||
ansible.builtin.command:
|
||||
cmd: "mv {{ ioc_base_dir }}/{{ orig_params_name }} {{ ioc_base_dir }}/{{ target_params_name }}"
|
||||
removes: "{{ ioc_base_dir }}/{{ orig_params_name }}" # Only runs if the original file exists
|
||||
|
||||
# Every @@token@@ is resolved from the central token table -- the token list
|
||||
# is not repeated here, so a new token needs no change to this file.
|
||||
- name: "[{{ target_service_name_lower }}] Collect deploy-time token values"
|
||||
ansible.builtin.set_fact:
|
||||
deploy_tokens: "{{ env_tokens | combine({'ioc_port': ioc_port}) }}"
|
||||
|
||||
- name: "[{{ target_service_name_lower }}] Substitute deploy-time tokens in the IOC files"
|
||||
ansible.builtin.replace:
|
||||
path: "{{ item.0 }}"
|
||||
regexp: '@@\s*{{ item.1.key }}\s*@@'
|
||||
replace: "{{ item.1.value | string }}"
|
||||
loop: >-
|
||||
{{ [ioc_base_dir ~ '/' ~ target_subs_name, ioc_base_dir ~ '/' ~ target_params_name]
|
||||
| product(deploy_tokens | dict2items) | list }}
|
||||
loop_control:
|
||||
label: "{{ item.0 | basename }} :: {{ item.1.key }}"
|
||||
|
||||
- name: "[{{ target_service_name_lower }}] Run ioc install"
|
||||
ansible.builtin.shell:
|
||||
cmd: "ioc install -V --facility sls --ioc AGEBD-CPCL-{{ service_name_upper }}{{ agebd_env_suffix_upper }} --clean"
|
||||
chdir: "{{ ioc_base_dir }}"
|
||||
changed_when: true
|
||||
register: ioc_install_result
|
||||
# Force a pipeline failure if the command output contains error keywords
|
||||
# (ioc install ... returns exit code 0 even on failure...)
|
||||
failed_when: >
|
||||
ioc_install_result.rc != 0 or
|
||||
"[error]" in ioc_install_result.stderr or
|
||||
"Unable to write" in ioc_install_result.stderr
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
- name: Init service name
|
||||
ansible.builtin.import_tasks: ../utils/init-service-name.yml
|
||||
|
||||
- name: Clone repo
|
||||
ansible.builtin.import_tasks: ../utils/clone-repo.yml
|
||||
|
||||
- name: Load deploy-time substitutions
|
||||
ansible.builtin.import_tasks: ../utils/load-substitutions.yml
|
||||
|
||||
- name: "[{{ target_service_name_lower }}] Establish environment naming conventions"
|
||||
ansible.builtin.set_fact:
|
||||
service_name_upper: "{{ target_service_name_lower | upper }}"
|
||||
|
||||
- name: "[{{ target_service_name_lower }}] Define file paths"
|
||||
ansible.builtin.set_fact:
|
||||
ioc_base_dir: "{{ workspace_dir }}/services/{{ target_service_name_lower }}/current/ioc"
|
||||
|
||||
- name: "[{{ target_service_name_lower }}] Restart IOC"
|
||||
ansible.builtin.shell:
|
||||
cmd: "ioc restart AGEBD-CPCL-{{ service_name_upper }}{{ agebd_env_suffix_upper }}"
|
||||
chdir: "{{ ioc_base_dir }}"
|
||||
changed_when: true
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
- name: Init service name
|
||||
ansible.builtin.import_tasks: ../utils/init-service-name.yml
|
||||
|
||||
- name: Clone repo
|
||||
ansible.builtin.import_tasks: ../utils/clone-repo.yml
|
||||
|
||||
- name: Get and set ioc_port
|
||||
ansible.builtin.import_tasks: ../utils/get-ioc-port.yml
|
||||
|
||||
- name: "[{{ ioc_port }}] Start IOC"
|
||||
ansible.builtin.shell: |
|
||||
sudo shellbox status
|
||||
sudo shellbox reload
|
||||
sudo shellbox status
|
||||
sudo shellbox start {{ ioc_port }}
|
||||
changed_when: true
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
- name: Initialize service name
|
||||
ansible.builtin.set_fact:
|
||||
# Uses preferred_service_name_lower if defined in vars, otherwise falls back to the CLI extra-vars
|
||||
target_service_name_lower: "{{ preferred_service_name_lower | default(service_name_lower) }}"
|
||||
|
||||
- name: Load deploy-time substitutions
|
||||
ansible.builtin.import_tasks: ../utils/load-substitutions.yml
|
||||
|
||||
- name: "[{{ target_service_name_lower }}] Define service specific paths and names"
|
||||
ansible.builtin.set_fact:
|
||||
svc_current_dir: "/sls/bd/hla/{{ agebd_env }}/services/{{ target_service_name_lower }}/current"
|
||||
service_unit_name: "AGEBD-SERVICE-{{ target_service_name_lower | upper }}.service"
|
||||
|
||||
- name: "[{{ target_service_name_lower }}] Ensure target directory structure exists"
|
||||
ansible.builtin.file:
|
||||
path: "{{ svc_current_dir }}"
|
||||
state: directory
|
||||
# ansible.builtin.shell: |
|
||||
# mkdir -p "{{ svc_current_dir }}"
|
||||
|
||||
# TODO: make sure this is what we want (essentially wipes existing dest dir). Does it remove .venv?
|
||||
- name: "[{{ target_service_name_lower }}] Sync service deployment files"
|
||||
ansible.posix.synchronize:
|
||||
src: "{{ repo_root }}/services/{{ target_service_name_lower }}/current/"
|
||||
dest: "{{ svc_current_dir }}/"
|
||||
delete: yes # This removes stale files on the target that no longer exist locally
|
||||
recursive: yes
|
||||
|
||||
# Every @@token@@ is resolved from the central token table -- the token list
|
||||
# is not repeated here, so a new token needs no change to this file.
|
||||
- name: "[{{ target_service_name_lower }}] Substitute deploy-time tokens in the systemd unit"
|
||||
ansible.builtin.replace:
|
||||
path: "{{ svc_current_dir }}/systemd/{{ service_unit_name }}"
|
||||
regexp: '@@\s*{{ item.key }}\s*@@'
|
||||
replace: "{{ item.value | string }}"
|
||||
loop: "{{ env_tokens | dict2items }}"
|
||||
loop_control:
|
||||
label: "{{ item.key }}"
|
||||
|
||||
# systemctl --user link --force "./{{ service_unit_name }}"
|
||||
- name: "[{{ target_service_name_lower }}] Symlink systemd unit file (Absolute Path)"
|
||||
ansible.builtin.file:
|
||||
src: "{{ svc_current_dir }}/systemd/{{ service_unit_name }}"
|
||||
dest: "~/.config/systemd/user/{{ service_unit_name }}"
|
||||
state: link
|
||||
force: yes # This overwrites the broken/existing symlink cleanly without error
|
||||
|
||||
# export XDG_RUNTIME_DIR=/run/user/$(id -u)
|
||||
# systemctl --user daemon-reload
|
||||
# systemctl --user enable "{{ service_unit_name }}"
|
||||
# systemctl --user restart "{{ service_unit_name }}"
|
||||
- name: "[{{ target_service_name_lower }}] Enable and Restart Systemd Service"
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ service_unit_name }}"
|
||||
scope: user
|
||||
daemon_reload: yes
|
||||
enabled: yes
|
||||
state: restarted
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ ansible_user_uid }}"
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
- name: Clone this repository
|
||||
ansible.builtin.git:
|
||||
repo: "git@gitea.psi.ch:sls/hla_framework_bd.git"
|
||||
dest: "{{ workspace_dir }}"
|
||||
accept_hostkey: true
|
||||
version: "{{ branch_name }}"
|
||||
force: true
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
- name: Read the service registry file
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ workspace_dir }}/config/services_registry.yml"
|
||||
register: remote_registry_file
|
||||
|
||||
- name: Parse the service registry YAML content into a fact
|
||||
ansible.builtin.set_fact:
|
||||
services_registry: "{{ remote_registry_file.content | b64decode | from_yaml }}"
|
||||
|
||||
- name: Extract the port from the services registry and save it as a persistent fact
|
||||
ansible.builtin.set_fact:
|
||||
ioc_port: "{{ services_registry.services | selectattr('name', 'equalto', target_service_name_lower) | map(attribute='ioc_port') | first | default(none) }}"
|
||||
|
||||
- name: Fail if the service or port wasn't found in the registry
|
||||
ansible.builtin.fail:
|
||||
msg: "Service '{{ target_service_name_lower }}' not found in services registry or port not set"
|
||||
when: ioc_port | trim == "" or ioc_port is none
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
- name: Initialize service name
|
||||
ansible.builtin.set_fact:
|
||||
# Uses preferred_service_name_lower if defined in vars, otherwise falls back to the CLI extra-vars
|
||||
target_service_name_lower: "{{ preferred_service_name_lower | default(service_name_lower) }}"
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
# Load the deploy-time token values for {{ agebd_env }} from the single source
|
||||
# of truth, config/environments.yml.
|
||||
#
|
||||
# The inventory says which environment it is; this file says what that
|
||||
# environment means. Do not reintroduce suffix values into the inventories.
|
||||
#
|
||||
# include_vars reads on the controller, where the repo checkout lives, so this
|
||||
# works for both local-connection plays and plays targeting remote hosts.
|
||||
#
|
||||
# Ansible substitutes tokens with ansible.builtin.replace rather than calling
|
||||
# agebd.subs, because these tasks also run on hosts that have no uv or Python
|
||||
# environment for this repo. The values and the token list still come from the
|
||||
# central config, so there is nothing to keep in sync by hand.
|
||||
|
||||
- name: Load the central substitution config
|
||||
ansible.builtin.include_vars:
|
||||
file: "{{ repo_root }}/config/environments.yml"
|
||||
name: substitutions
|
||||
|
||||
- name: Fail if this environment is not declared centrally
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
Environment '{{ agebd_env }}' is not declared in config/environments.yml.
|
||||
Declared: {{ substitutions.environments.keys() | list | join(', ') }}
|
||||
when: agebd_env not in substitutions.environments
|
||||
|
||||
- name: Select the deploy-time token values for this environment
|
||||
ansible.builtin.set_fact:
|
||||
env_tokens: "{{ substitutions.environments[agebd_env] }}"
|
||||
agebd_env_suffix_upper: "{{ substitutions.environments[agebd_env].agebd_env_suffix_upper }}"
|
||||
agebd_env_suffix_lower: "{{ substitutions.environments[agebd_env].agebd_env_suffix_lower }}"
|
||||
@@ -7,6 +7,10 @@ set --
|
||||
# PSI_FACILITY=SLS
|
||||
|
||||
SERVICES_DIR=/sls/bd/hla/${AGEBD_ENV}/services
|
||||
# These two derivations are the one place that deliberately does not use
|
||||
# agebd.names.ServiceName: this runs on the deploy host before any Python
|
||||
# environment for the service exists (the venv is built further down), so it
|
||||
# cannot import agebd. Keep the rules here in step with agebd/names.py.
|
||||
SVC_NAME_LOWER=${SVC_NAME,,}
|
||||
SVC_NAME_LOWER_UNDERSCORES="${SVC_NAME_LOWER//-/_}"
|
||||
SVC_DIR=${SERVICES_DIR}/${SVC_NAME_LOWER}
|
||||
|
||||
@@ -9,8 +9,6 @@ authors = [
|
||||
|
||||
dependencies = [
|
||||
"agebd",
|
||||
"ansible>=8.7.0",
|
||||
"ansible-runner>=2.4.3",
|
||||
"copier>=9.10.3",
|
||||
"pydantic>=2.13.4",
|
||||
"pydantic-settings>=2.14.2",
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import ansible_runner
|
||||
import typer
|
||||
|
||||
from agebd.utils import get_git_root
|
||||
from core.enums import IOC_ENV
|
||||
|
||||
ioc = typer.Typer(no_args_is_help=True)
|
||||
|
||||
REPO_ROOT = get_git_root(__file__)
|
||||
|
||||
|
||||
def run_playbook(playbook: str, env: IOC_ENV, vars: list[str]):
|
||||
env_str = str(env)
|
||||
if env_str not in ["dev", "prod"]:
|
||||
raise ValueError("env must be 'dev' or 'prod'")
|
||||
|
||||
current_env = os.environ.copy()
|
||||
current_env["ANSIBLE_CONFIG"] = f"{REPO_ROOT}/ansible/ansible.cfg"
|
||||
|
||||
extra_vars = [f"--extra-vars={var}" for var in vars]
|
||||
out, err, rc = ansible_runner.run_command(
|
||||
executable_cmd=f"ansible-playbook",
|
||||
cmdline_args=[
|
||||
playbook,
|
||||
"-i",
|
||||
f"{REPO_ROOT}/ansible/inventories/{env_str}/hosts.yml",
|
||||
*extra_vars,
|
||||
"-v",
|
||||
],
|
||||
envvars=current_env,
|
||||
input_fd=sys.stdin,
|
||||
output_fd=sys.stdout,
|
||||
error_fd=sys.stderr,
|
||||
)
|
||||
print("rc: {}".format(rc))
|
||||
print("out: {}".format(out))
|
||||
print("err: {}".format(err))
|
||||
@@ -6,14 +6,6 @@ class ServiceStatus(str, Enum):
|
||||
DISABLED = "disabled"
|
||||
|
||||
|
||||
class IOC_ENV(str, Enum):
|
||||
PROD = "prod"
|
||||
DEV = "dev"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
class GuiOption(str, Enum):
|
||||
NEW = "new"
|
||||
EXISTING = "existing"
|
||||
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
import typer
|
||||
|
||||
from agebd.utils import get_git_root
|
||||
from config.paths import Paths
|
||||
from core.ansible import run_playbook
|
||||
from core.enums import IOC_ENV
|
||||
from models import ServiceRegistry
|
||||
|
||||
ioc = typer.Typer(no_args_is_help=True)
|
||||
|
||||
REPO_ROOT = get_git_root(__file__)
|
||||
|
||||
ANSIBLE_PLAYBOOKS_DIR = REPO_ROOT / "ansible" / "playbooks"
|
||||
IOC_PLAYBOOK_INSTALL = ANSIBLE_PLAYBOOKS_DIR / "ioc-install.yml"
|
||||
IOC_PLAYBOOK_RESTART = ANSIBLE_PLAYBOOKS_DIR / "ioc-restart.yml"
|
||||
IOC_PLAYBOOK_START = ANSIBLE_PLAYBOOKS_DIR / "ioc-start.yml"
|
||||
|
||||
SERVICE_NAME_HELP = "Service name in lower case"
|
||||
|
||||
|
||||
def _resolve_branch_name(branch_name: str | None, service_name_lower: str) -> str:
|
||||
return branch_name or f"feature/add-service-{service_name_lower}"
|
||||
|
||||
|
||||
def _get_registry_services():
|
||||
paths = Paths()
|
||||
registry = ServiceRegistry.read_from_file(paths.service_registry_file)
|
||||
return registry.get_services()
|
||||
|
||||
|
||||
@ioc.command(no_args_is_help=True)
|
||||
def install(
|
||||
env: IOC_ENV = typer.Option(..., "--env"),
|
||||
service_name_lower: str = typer.Option(..., "--service-name", "-s", help=SERVICE_NAME_HELP),
|
||||
branch_name: str | None = typer.Option(
|
||||
None, "--branch-name", "-b", help="Default is 'feature/add-service-{service-name}'"
|
||||
),
|
||||
):
|
||||
branch_name = _resolve_branch_name(branch_name, service_name_lower)
|
||||
run_playbook(
|
||||
playbook=str(IOC_PLAYBOOK_INSTALL),
|
||||
env=env,
|
||||
vars=[f"branch_name={branch_name}", f"service_name_lower={service_name_lower}"],
|
||||
)
|
||||
|
||||
|
||||
@ioc.command(no_args_is_help=True)
|
||||
def install_all(
|
||||
env: IOC_ENV = typer.Option(..., "--env"),
|
||||
):
|
||||
for service in _get_registry_services():
|
||||
run_playbook(
|
||||
playbook=str(IOC_PLAYBOOK_INSTALL),
|
||||
env=env,
|
||||
vars=["branch_name=main", f"service_name_lower={service.name_lower}"],
|
||||
)
|
||||
|
||||
|
||||
@ioc.command(no_args_is_help=True)
|
||||
def start(
|
||||
env: IOC_ENV = typer.Option(..., "--env"),
|
||||
service_name_lower: str = typer.Option(..., "--service-name", "-s", help=SERVICE_NAME_HELP),
|
||||
branch_name: str | None = typer.Option(
|
||||
None, "--branch-name", "-b", help="Default is 'feature/add-service-{service-name}'"
|
||||
),
|
||||
):
|
||||
branch_name = _resolve_branch_name(branch_name, service_name_lower)
|
||||
run_playbook(
|
||||
playbook=str(IOC_PLAYBOOK_START),
|
||||
env=env,
|
||||
vars=[f"service_name_lower={service_name_lower}", f"branch_name={branch_name}"],
|
||||
)
|
||||
|
||||
|
||||
@ioc.command()
|
||||
def start_all(
|
||||
env: IOC_ENV = typer.Option(..., "--env"),
|
||||
):
|
||||
for service in _get_registry_services():
|
||||
run_playbook(
|
||||
playbook=str(IOC_PLAYBOOK_START),
|
||||
env=env,
|
||||
vars=[f"service_name_lower={service.name_lower}"],
|
||||
)
|
||||
|
||||
|
||||
@ioc.command(no_args_is_help=True)
|
||||
def restart(
|
||||
env: IOC_ENV = typer.Option(..., "--env"),
|
||||
service_name_lower: str = typer.Option(..., "--service-name", "-s", help=SERVICE_NAME_HELP),
|
||||
branch_name: str | None = typer.Option(
|
||||
None, "--branch-name", "-b", help="Default is 'feature/add-service-{service-name}'"
|
||||
),
|
||||
):
|
||||
branch_name = _resolve_branch_name(branch_name, service_name_lower)
|
||||
run_playbook(
|
||||
playbook=str(IOC_PLAYBOOK_RESTART),
|
||||
env=env,
|
||||
vars=[f"branch_name={branch_name}", f"service_name_lower={service_name_lower}"],
|
||||
)
|
||||
|
||||
|
||||
@ioc.command(no_args_is_help=True)
|
||||
def restart_all(
|
||||
env: IOC_ENV = typer.Option(..., "--env"),
|
||||
):
|
||||
for service in _get_registry_services():
|
||||
run_playbook(
|
||||
playbook=str(IOC_PLAYBOOK_RESTART),
|
||||
env=env,
|
||||
vars=["branch_name=main", f"service_name_lower={service.name_lower}"],
|
||||
)
|
||||
@@ -2,7 +2,6 @@ import logging
|
||||
|
||||
import typer
|
||||
|
||||
import ioc
|
||||
import service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -10,7 +9,6 @@ logger = logging.getLogger(__name__)
|
||||
cli = typer.Typer(no_args_is_help=True, add_completion=False)
|
||||
|
||||
cli.add_typer(service.service, name="service")
|
||||
cli.add_typer(ioc.ioc, name="ioc")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+22
-11
@@ -1,34 +1,48 @@
|
||||
import re
|
||||
|
||||
from agebd.names import ServiceName
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
from core.enums import ServiceStatus
|
||||
|
||||
|
||||
class Service(BaseModel):
|
||||
"""
|
||||
A service's registry entry.
|
||||
|
||||
Every spelling of the name is delegated to agebd.names.ServiceName, which
|
||||
is also what the CI and deploy scripts use.
|
||||
"""
|
||||
|
||||
name: str
|
||||
ioc_port: int
|
||||
status: ServiceStatus
|
||||
|
||||
@property
|
||||
def service_name(self) -> ServiceName:
|
||||
return ServiceName(self.name)
|
||||
|
||||
@property
|
||||
def name_camel(self):
|
||||
return self.name
|
||||
return self.service_name.camel
|
||||
|
||||
@property
|
||||
def name_upper(self):
|
||||
return self.name.upper()
|
||||
return self.service_name.upper
|
||||
|
||||
@property
|
||||
def name_lower(self):
|
||||
return self.name.lower()
|
||||
return self.service_name.lower
|
||||
|
||||
@property
|
||||
def name_lower_underscores(self):
|
||||
return self.name.replace("-", "_").lower()
|
||||
return self.service_name.lower_underscores
|
||||
|
||||
@property
|
||||
def dir_name(self):
|
||||
return self.name.lower()
|
||||
return self.service_name.dir_name
|
||||
|
||||
@property
|
||||
def ioc_name(self):
|
||||
return self.service_name.ioc_name
|
||||
|
||||
def to_dict(self):
|
||||
return {"name": self.name_camel, "ioc_port": self.ioc_port, "status": str(self.status)}
|
||||
@@ -39,8 +53,5 @@ class Service(BaseModel):
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name_chars(cls, value: str) -> str:
|
||||
if not re.match(r"^[a-zA-Z0-9-]+$", value):
|
||||
raise ValueError(
|
||||
"name can only contain uppercase and lowercase letters, numbers, and dashes"
|
||||
)
|
||||
ServiceName(value)
|
||||
return value
|
||||
|
||||
@@ -3,6 +3,8 @@ from pathlib import Path
|
||||
import yaml
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agebd.subs import find_service_entry
|
||||
|
||||
from core.enums import ServiceStatus
|
||||
from models.service import Service
|
||||
|
||||
@@ -26,12 +28,15 @@ class ServiceRegistry(BaseModel):
|
||||
return [Service(**svc_data) for svc_data in self.services]
|
||||
|
||||
def assert_service_exists(self, name: str) -> Service:
|
||||
for svc_data in self.services:
|
||||
service = Service(**svc_data)
|
||||
if name == service.name_camel:
|
||||
return service
|
||||
|
||||
raise ValueError(f"Could not find service with name: {name}")
|
||||
"""
|
||||
Look a service up by name. Matching goes through
|
||||
agebd.subs.find_service_entry so that the CLI, the CI scripts and the
|
||||
deploy scripts all agree on what "the service named X" means.
|
||||
"""
|
||||
entry = find_service_entry(self.services, name)
|
||||
if entry is None:
|
||||
raise ValueError(f"Could not find service with name: {name}")
|
||||
return Service(**entry)
|
||||
|
||||
def add_service(self, name: str) -> Service:
|
||||
svc = Service(
|
||||
|
||||
Generated
-147
@@ -39,8 +39,6 @@ version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "agebd" },
|
||||
{ name = "ansible" },
|
||||
{ name = "ansible-runner" },
|
||||
{ name = "copier" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
@@ -62,8 +60,6 @@ dev = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "agebd", editable = "../packages/agebd" },
|
||||
{ name = "ansible", specifier = ">=8.7.0" },
|
||||
{ name = "ansible-runner", specifier = ">=2.4.3" },
|
||||
{ name = "copier", specifier = ">=9.10.3" },
|
||||
{ name = "pydantic", specifier = ">=2.13.4" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.14.2" },
|
||||
@@ -100,49 +96,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansible"
|
||||
version = "10.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ansible-core" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/64/29fdff6fe7682342adb54802c1cd90b2272d382e1743089af88f90a1d986/ansible-10.7.0.tar.gz", hash = "sha256:59d29e3de1080e740dfa974517d455217601b16d16880314d9be26145c68dc22", size = 41256974, upload-time = "2024-12-03T18:04:25.794Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/95/cb8944902a2cdd94b1e19ff73695548679a388b9c473dc63c8dc64ffea3a/ansible-10.7.0-py3-none-any.whl", hash = "sha256:0089f08e047ceb70edd011be009f5c6273add613fbe491e9697c0556c989d8ea", size = 51576038, upload-time = "2024-12-03T18:04:20.065Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansible-core"
|
||||
version = "2.17.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "resolvelib" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/80/2925a0564f6f99a8002c3be3885b83c3a1dc5f57ebf00163f528889865f5/ansible_core-2.17.14.tar.gz", hash = "sha256:7c17fee39f8c29d70e3282a7e9c10bd70d5cd4fd13ddffc5dcaa52adbd142ff8", size = 3119687, upload-time = "2025-09-08T18:28:03.158Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/86/29/d694562f1a875b50aa74f691521fe493704f79cf1938cd58f28f7e2327d2/ansible_core-2.17.14-py3-none-any.whl", hash = "sha256:34a49582a57c2f2af17ede2cefd3b3602a2d55d22089f3928570d52030cafa35", size = 2189656, upload-time = "2025-09-08T18:28:00.375Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansible-runner"
|
||||
version = "2.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
{ name = "pexpect" },
|
||||
{ name = "python-daemon" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/68/4f/62222b42b52cc771db51ccc77ffac19cc5f0196ff61de7c93585845a819b/ansible_runner-2.4.3.tar.gz", hash = "sha256:5f3025529bb968fdc3b627457dd8d418dbf9d53bc73735d50e69c28544d53031", size = 154148, upload-time = "2026-03-16T18:42:14.861Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/89/fa7f81db6134caa455c9351373ee934b5fdeed212eb939d6c42d80d683d7/ansible_runner-2.4.3-py3-none-any.whl", hash = "sha256:cdac6daa151a50084ffda710e769db23fa975fc0507796191d7708831b286e37", size = 80257, upload-time = "2026-03-16T18:42:13.72Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asttokens"
|
||||
version = "3.0.2"
|
||||
@@ -152,29 +105,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
@@ -209,44 +139,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/2c/68dec88ee26f96af2d4d8ee9d8567313a0826f4fd6bc5ae4527be670d354/copier-9.17.0-py3-none-any.whl", hash = "sha256:fe4e7b59faf4c0e1386eccbb79c6797e6df33ca20d5de49a8372571d8669f2ad", size = 65954, upload-time = "2026-07-13T14:33:03.301Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "49.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "decorator"
|
||||
version = "5.3.1"
|
||||
@@ -408,15 +300,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl", hash = "sha256:e1082f5564917649c76fed239117820610516ec10f87735d0338688800a55b34", size = 18975, upload-time = "2022-06-30T14:08:49.571Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lockfile"
|
||||
version = "0.12.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/17/47/72cb04a58a35ec495f96984dddb48232b551aafb95bde614605b754fe6f7/lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799", size = 20874, upload-time = "2015-11-25T18:29:58.279Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/22/9460e311f340cb62d26a38c419b1381b8593b0bb6b5d1f056938b086d362/lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa", size = 13564, upload-time = "2015-11-25T18:29:51.462Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.2.0"
|
||||
@@ -599,15 +482,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.13.4"
|
||||
@@ -750,18 +624,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-daemon"
|
||||
version = "3.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "lockfile" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/37/4f10e37bdabc058a32989da2daf29e57dc59dbc5395497f3d36d5f5e2694/python_daemon-3.1.2.tar.gz", hash = "sha256:f7b04335adc473de877f5117e26d5f1142f4c9f7cd765408f0877757be5afbf4", size = 71576, upload-time = "2024-12-03T08:41:07.843Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/3c/b88167e2d6785c0e781ee5d498b07472aeb9b6765da3b19e7cc9e0813841/python_daemon-3.1.2-py3-none-any.whl", hash = "sha256:b906833cef63502994ad48e2eab213259ed9bb18d54fa8774dcba2ff7864cec6", size = 30872, upload-time = "2024-12-03T08:41:03.322Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.2"
|
||||
@@ -800,15 +662,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resolvelib"
|
||||
version = "1.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/10/f699366ce577423cbc3df3280063099054c23df70856465080798c6ebad6/resolvelib-1.0.1.tar.gz", hash = "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309", size = 21065, upload-time = "2023-03-09T05:10:38.292Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/fc/e9ccf0521607bcd244aa0b3fbd574f71b65e9ce6a112c83af988bbbe2e23/resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf", size = 17194, upload-time = "2023-03-09T05:10:36.214Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "15.0.0"
|
||||
|
||||
@@ -65,11 +65,9 @@ locations:
|
||||
- templates
|
||||
- cli/tests/fixtures
|
||||
# The substitution machinery itself: code that renders, generates or
|
||||
# documents tokens. Naming a token here is expected; nothing is resolved.
|
||||
# These paths use {{ }} as their own template language (Ansible, copier), so
|
||||
# the leftover-delimiter check does not apply to them.
|
||||
# documents tokens. Naming a token here is expected; nothing is resolved,
|
||||
# and the leftover-delimiter check does not apply.
|
||||
references:
|
||||
- ansible/tasks/*
|
||||
- .gitea/scripts
|
||||
- cli/src/models
|
||||
- packages/agebd/src/agebd
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# Ansible
|
||||
|
||||
## Running a playbook manually
|
||||
|
||||
Set vars on command line:
|
||||
|
||||
```
|
||||
ansible-playbook ansible/playbooks/add-new-service.yml \
|
||||
-i ansible/hosts.yml \
|
||||
--extra-vars="repo_root=${{ github.workspace }} agebd_env=dev service_name_lower=<service dir name>" \
|
||||
-v
|
||||
```
|
||||
|
||||
### Install an IOC
|
||||
```
|
||||
ansible-playbook ansible/playbooks/ioc-install.yml \
|
||||
-i ansible/hosts.yml \
|
||||
--extra-vars="agebd_env=dev branch_name=feature/add-service-playground service_name_lower=playground ioc_port=50003" \
|
||||
-v
|
||||
```
|
||||
|
||||
### Start an IOC
|
||||
```
|
||||
ansible-playbook ansible/playbooks/ioc-start.yml \
|
||||
-i ansible/hosts.yml \
|
||||
--extra-vars="service_name_lower=playground ioc_port=50003" \
|
||||
-v
|
||||
```
|
||||
|
||||
### Restart an IOC
|
||||
```
|
||||
ansible-playbook ansible/playbooks/ioc-restart.yml \
|
||||
-i ansible/hosts.yml \
|
||||
--extra-vars="agebd_env=dev branch_name=feature/add-service-playground service_name_lower=playground" \
|
||||
-v
|
||||
```
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
The one definition of how a service name is spelled in each context.
|
||||
|
||||
A service has a single canonical CamelCase name, recorded in
|
||||
config/services_registry.yml. Everything else is derived from it by a fixed
|
||||
rule: the directory it lives in, its Python module, its EPICS IOC, its systemd
|
||||
unit. Those rules live here so that the CLI, the CI scripts and the deploy
|
||||
scripts cannot drift apart on them.
|
||||
|
||||
The environment suffix is deliberately not part of any name here. It is a
|
||||
deploy-time token (see agebd.subs), applied to the base names below.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
#: Prefix of every EPICS IOC in this framework.
|
||||
IOC_PREFIX = "AGEBD-CPCL"
|
||||
|
||||
#: Prefix of every systemd unit in this framework.
|
||||
SYSTEMD_UNIT_PREFIX = "AGEBD-SERVICE"
|
||||
|
||||
#: Prefix of every service's Python package.
|
||||
PACKAGE_PREFIX = "agebd"
|
||||
|
||||
VALID_NAME = re.compile(r"^[a-zA-Z0-9-]+$")
|
||||
|
||||
|
||||
class InvalidServiceName(ValueError):
|
||||
"""The name is not a legal service name."""
|
||||
|
||||
|
||||
class ServiceName:
|
||||
"""
|
||||
A service's canonical name and every form derived from it.
|
||||
|
||||
>>> name = ServiceName("TuneFBx")
|
||||
>>> name.dir_name, name.module_name, name.ioc_name
|
||||
('tunefbx', 'agebd_tunefbx', 'AGEBD-CPCL-TUNEFBX')
|
||||
"""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
if not isinstance(name, str) or not VALID_NAME.match(name):
|
||||
raise InvalidServiceName(
|
||||
f"Invalid service name {name!r}: a service name may only contain "
|
||||
f"uppercase and lowercase letters, numbers, and dashes."
|
||||
)
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def camel(self) -> str:
|
||||
"""The canonical name, as written in the registry and shown in the GUI."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def upper(self) -> str:
|
||||
return self._name.upper()
|
||||
|
||||
@property
|
||||
def lower(self) -> str:
|
||||
return self._name.lower()
|
||||
|
||||
@property
|
||||
def lower_underscores(self) -> str:
|
||||
return self.lower.replace("-", "_")
|
||||
|
||||
@property
|
||||
def dir_name(self) -> str:
|
||||
"""The directory under services/ that holds this service."""
|
||||
return self.lower
|
||||
|
||||
@property
|
||||
def module_name(self) -> str:
|
||||
"""The Python package implementing this service."""
|
||||
return f"{PACKAGE_PREFIX}_{self.lower_underscores}"
|
||||
|
||||
@property
|
||||
def ioc_name(self) -> str:
|
||||
"""
|
||||
The IOC's base name. Deployments append the environment suffix token.
|
||||
"""
|
||||
return f"{IOC_PREFIX}-{self.upper}"
|
||||
|
||||
@property
|
||||
def systemd_unit_name(self) -> str:
|
||||
"""
|
||||
The systemd unit file name. Unit names carry no environment suffix:
|
||||
dev and prod run on different hosts.
|
||||
"""
|
||||
return f"{SYSTEMD_UNIT_PREFIX}-{self.upper}.service"
|
||||
|
||||
def matches(self, other) -> bool:
|
||||
"""
|
||||
Whether ``other`` names this service. Comparison is case-insensitive,
|
||||
because callers hold the name in whatever casing their context uses.
|
||||
"""
|
||||
try:
|
||||
return ServiceName(str(other)).lower == self.lower
|
||||
except InvalidServiceName:
|
||||
return False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.camel
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ServiceName({self.camel!r})"
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if isinstance(other, ServiceName):
|
||||
return self.lower == other.lower
|
||||
return NotImplemented
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.lower)
|
||||
@@ -36,6 +36,7 @@ from typing import Iterable, Iterator, Optional, Union
|
||||
|
||||
import yaml
|
||||
|
||||
from agebd.names import InvalidServiceName, ServiceName
|
||||
from agebd.utils import get_git_root
|
||||
|
||||
CONFIG_DIR_ENV_VAR = "AGEBD_CONFIG_DIR"
|
||||
@@ -306,19 +307,32 @@ class Substitutions:
|
||||
def read_ioc_port(registry_file: Union[str, Path], service_name: str) -> int:
|
||||
"""
|
||||
Look up a service's IOC port in the services registry.
|
||||
|
||||
Registry names are CamelCase; callers pass whatever casing they have, so
|
||||
the comparison is case-insensitive.
|
||||
"""
|
||||
with open(registry_file) as f:
|
||||
registry = yaml.safe_load(f) or {}
|
||||
|
||||
wanted = service_name.lower()
|
||||
for entry in registry.get("services") or []:
|
||||
if str(entry.get("name", "")).lower() == wanted:
|
||||
return entry["ioc_port"]
|
||||
entry = find_service_entry(registry.get("services") or [], service_name)
|
||||
if entry is None:
|
||||
raise SubstitutionError(f"Service '{service_name}' not found in {registry_file}")
|
||||
return entry["ioc_port"]
|
||||
|
||||
raise SubstitutionError(f"Service '{service_name}' not found in {registry_file}")
|
||||
|
||||
def find_service_entry(services: Iterable, service_name: str):
|
||||
"""
|
||||
Find a service's registry entry by name, or None.
|
||||
|
||||
Registry names are CamelCase; callers hold whatever casing their context
|
||||
uses, so matching goes through agebd.names.ServiceName.
|
||||
"""
|
||||
try:
|
||||
wanted = ServiceName(service_name)
|
||||
except InvalidServiceName:
|
||||
return None
|
||||
|
||||
for entry in services:
|
||||
if wanted.matches(entry.get("name", "")):
|
||||
return entry
|
||||
return None
|
||||
|
||||
|
||||
def find_tokens(text: str) -> set:
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import pytest
|
||||
|
||||
from agebd.names import InvalidServiceName, ServiceName
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name, upper, lower, underscores",
|
||||
[
|
||||
("Tune", "TUNE", "tune", "tune"),
|
||||
("TauBPM", "TAUBPM", "taubpm", "taubpm"),
|
||||
("TuneFBx", "TUNEFBX", "tunefbx", "tunefbx"),
|
||||
("OpticsFF-X02S", "OPTICSFF-X02S", "opticsff-x02s", "opticsff_x02s"),
|
||||
],
|
||||
)
|
||||
def test_case_forms(name, upper, lower, underscores):
|
||||
service = ServiceName(name)
|
||||
|
||||
assert service.camel == name
|
||||
assert service.upper == upper
|
||||
assert service.lower == lower
|
||||
assert service.lower_underscores == underscores
|
||||
|
||||
|
||||
def test_derived_names():
|
||||
service = ServiceName("OpticsFF-X02S")
|
||||
|
||||
assert service.dir_name == "opticsff-x02s"
|
||||
assert service.module_name == "agebd_opticsff_x02s"
|
||||
assert service.ioc_name == "AGEBD-CPCL-OPTICSFF-X02S"
|
||||
assert service.systemd_unit_name == "AGEBD-SERVICE-OPTICSFF-X02S.service"
|
||||
|
||||
|
||||
def test_derived_names_carry_no_environment_suffix():
|
||||
"""The suffix is a deploy-time token, applied to these base names."""
|
||||
service = ServiceName("Tune")
|
||||
|
||||
assert "DEV" not in service.ioc_name
|
||||
assert "DEV" not in service.systemd_unit_name
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["Tune", "tune", "TUNE", "tUnE"])
|
||||
def test_matches_is_case_insensitive(name):
|
||||
assert ServiceName("Tune").matches(name)
|
||||
|
||||
|
||||
def test_does_not_match_a_different_service():
|
||||
assert not ServiceName("Tune").matches("TuneBump")
|
||||
|
||||
|
||||
def test_does_not_match_an_invalid_name():
|
||||
assert not ServiceName("Tune").matches("Tune!")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["", "Tune Bump", "Tune_Bump", "Tune!", "Tune/Bump"])
|
||||
def test_invalid_names_are_rejected(name):
|
||||
with pytest.raises(InvalidServiceName):
|
||||
ServiceName(name)
|
||||
|
||||
|
||||
def test_equality_and_hashing_ignore_case():
|
||||
assert ServiceName("Tune") == ServiceName("TUNE")
|
||||
assert len({ServiceName("Tune"), ServiceName("tune")}) == 1
|
||||
|
||||
|
||||
def test_str_is_the_canonical_name():
|
||||
assert str(ServiceName("TauBPM")) == "TauBPM"
|
||||
@@ -1,8 +1,8 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from agebd.service.pvs import BasePVs
|
||||
from agebd.runner import CallbackRunner, PeriodicRunner
|
||||
from agebd.service.base import BaseService
|
||||
from agebd.service.pvs import BasePVs
|
||||
|
||||
|
||||
class DummyPV:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from agebd.service.pvs import BasePVs
|
||||
from agebd.service.base import BaseService
|
||||
from agebd.service.pvs import BasePVs
|
||||
|
||||
|
||||
class DummyPV:
|
||||
|
||||
+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