feature: dynamicallt allocate port and save in service registry. Update iocs overview on service creation
Deploy bin / deploy (push) Successful in 5s
Deploy agebd python package / deploy (push) Successful in 2s

This commit is contained in:
Benjamin Labrecque
2026-07-08 14:40:18 +02:00
parent 9c0709989e
commit 6960b10ce0
13 changed files with 142 additions and 34 deletions
+1
View File
@@ -19,6 +19,7 @@
branch_name: "feature/add-service-{{ new_service_dir_name }}"
import_playbook: ../plays/service/service-deploy-and-restart.yml
# TODO: ioc_port
# - name: Install New IOC
# vars:
# branch_name: "feature/add-service-{{ new_service_dir_name }}"
+6 -3
View File
@@ -50,11 +50,14 @@
removes: "{{ ioc_base_dir }}/{{ orig_params_name }}" # Only runs if the original file exists
# Replace {{ agebd_env }} inside the newly renamed subs file
- name: "[{{ service_dir_name }}] Substitute environment token inside subs file"
- name: "[{{ service_dir_name }}] Substitute environment tokens inside subs file"
ansible.builtin.replace:
path: "{{ ioc_base_dir }}/{{ target_subs_name }}"
regexp: '\{\{\s*agebd_env\s*\}\}'
replace: "{{ env_suffix_upper }}"
regexp: "{{ item.regexp }}"
replace: "{{ item.replace | string }}"
with_items:
- { regexp: '\{\{\s*agebd_env\s*\}\}', replace: "{{ env_suffix_upper }}" }
- { regexp: '\{\{\s*ioc_port\s*\}\}', replace: "{{ ioc_port }}" }
# Replace {{ agebd_env }} inside the newly renamed params file
- name: "[{{ service_dir_name }}] Substitute environment token inside params file"
+41 -5
View File
@@ -15,10 +15,13 @@ MASTER_SERVICE_DIR = REPO_ROOT / "services" / "000-master" / "current"
MASTER_SERVICE_CONFIG_DIR = MASTER_SERVICE_DIR / "app" / "config"
MASTER_HLA_NAMES_FILENAME = MASTER_SERVICE_CONFIG_DIR / "hla_names.yml"
IOCS_OVERVIEW_FILENAME = REPO_ROOT / "docs" / "ioc" / "ioc_overview.md"
class Service(BaseModel):
id: int
name: str
ioc_port: int
status: ServiceStatus
@property
@@ -39,7 +42,7 @@ class Service(BaseModel):
return f"{self.id:03d}"
def to_dict(self):
return {"name": self.name_lower, "status": str(self.status)}
return {"name": self.name_lower, "ioc_port": self.ioc_port, "status": str(self.status)}
class Config:
use_enum_values = True
@@ -56,7 +59,8 @@ class ServiceRegistry(BaseModel):
config_path: ClassVar[str] = str(SERVICE_REGISTRY_FILENAME)
next_available_id: int
services: dict[int, dict]
next_available_ioc_port: int
services: dict[str, dict]
@classmethod
def read_from_config(cls) -> "ServiceRegistry":
@@ -67,12 +71,18 @@ class ServiceRegistry(BaseModel):
return registry
def get_services(self) -> list[Service]:
return [Service(id=svc_id, **svc_data) for svc_id, svc_data in self.services.items()]
return [Service(id=int(svc_id), **svc_data) for svc_id, svc_data in self.services.items()]
def add_service(self, name: str) -> Service:
svc = Service(id=self.next_available_id, name=name, status=ServiceStatus.ACTIVE)
self.services.update({svc.id: svc.to_dict()})
svc = Service(
id=self.next_available_id,
name=name,
ioc_port=self.next_available_ioc_port,
status=ServiceStatus.ACTIVE,
)
self.services.update({svc.id_str: svc.to_dict()})
self.next_available_id += 1
self.next_available_ioc_port += 1
return svc
@@ -100,3 +110,29 @@ class MasterHLANames(BaseModel):
def write_to_config(self) -> None:
with open(self.config_path, "w") as f:
yaml.safe_dump(self.model_dump(), f, indent=4)
class IocsOverview(BaseModel):
config_path: ClassVar[str] = str(IOCS_OVERVIEW_FILENAME)
content: str
@classmethod
def read_from_file(cls) -> "IocsOverview":
with open(cls.config_path, "r", encoding="utf-8") as f:
s = f.read()
return IocsOverview(content=s)
def add_ioc(self, service: Service, description: str):
new_row = f"| AGEBD-CPCL-{service.name_upper} | {service.ioc_port} | {description} |\n"
# Ensuring it starts on a clean new line if the file didn't end with one
if not self.content.endswith("\n"):
self.content += "\n"
self.content += new_row
def write_to_file(self):
with open(self.config_path, "w") as f:
f.write(self.content)
+8 -10
View File
@@ -14,7 +14,7 @@ from core.git import (
git_push_changes,
switch_branch,
)
from core.models import MasterHLANames, Service, ServiceRegistry
from core.models import IocsOverview, MasterHLANames, Service, ServiceRegistry
from core.utils import get_git_root
logger = logging.getLogger(__name__)
@@ -30,13 +30,9 @@ MASTER_IOC_SUBS_FILE = MASTER_SERVICE_DIR / "ioc" / "AGEBD-CPCL-MASTER_main.subs
class ServiceCreator:
def __init__(
self,
ioc_port: int,
user: str,
) -> None:
self.ioc_port = ioc_port
def __init__(self, user: str, ioc_description: str) -> None:
self.user = user
self.ioc_description = ioc_description
def add_service(self, name: str):
repo = git.Repo(REPO_ROOT)
@@ -50,6 +46,9 @@ class ServiceCreator:
hla_names_old = MasterHLANames.read_from_config()
hla_names = MasterHLANames.read_from_config()
hla_names.add_hla_name(service.name_upper)
iocs_overview_old = IocsOverview.read_from_file()
iocs_overview = IocsOverview.read_from_file()
iocs_overview.add_ioc(service, description=self.ioc_description)
branch_name = f"feature/add-service-{service.dir_name}"
delete_local_branch(repo, branch_name)
@@ -58,12 +57,12 @@ class ServiceCreator:
try:
self._render_new_service_templates(
service=service,
ioc_port=self.ioc_port,
user=self.user,
)
self._generate_uv_lock(service=service)
registry.write_to_config()
hla_names.write_to_config()
iocs_overview.write_to_file()
self._append_to_master_ioc_subs(
service_name=service.name_upper,
@@ -87,13 +86,13 @@ class ServiceCreator:
shutil.rmtree(Path(SERVICE_DEST_DIR / service.dir_name), ignore_errors=True)
registry_old.write_to_config()
hla_names_old.write_to_config()
iocs_overview_old.write_to_file()
switch_branch(repo, original_branch)
raise e
def _render_new_service_templates(
self,
service: Service,
ioc_port: int,
user: str,
):
"""
@@ -106,7 +105,6 @@ class ServiceCreator:
"service_id": service.id_str,
"service_name_upper": service.name_upper,
"service_name_lower": service.name_lower,
"ioc_port": ioc_port,
"user": user,
},
)
+4 -2
View File
@@ -12,13 +12,15 @@ REPO_ROOT = get_git_root(__file__)
@service.command()
def add(
name: str = typer.Option(..., "--name", "-n"),
ioc_port: int = typer.Option(..., "--ioc-port", "-p"),
user: str = typer.Option(..., "--user", "-u"),
ioc_description: str = typer.Option(
..., "--ioc-description", "-d", help="See <repo-root>/docs/ioc/ioc_overview.md for examples"
),
):
init_logging()
ServiceCreator(
ioc_port=ioc_port, # TODO: read from registry
user=user, # TODO: read from 'whoami'?
ioc_description=ioc_description,
).add_service(name=name)
Generated
+71 -8
View File
@@ -2,7 +2,16 @@ version = 1
revision = 3
requires-python = ">=3.9"
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version >= '3.14' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.10.*'",
"python_full_version < '3.10'",
]
@@ -117,7 +126,16 @@ name = "copier"
version = "9.16.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version >= '3.14' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.10.*'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version >= '3.10'" },
@@ -166,7 +184,7 @@ name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
@@ -224,7 +242,16 @@ name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version >= '3.14' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.10.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
@@ -276,7 +303,16 @@ name = "markdown-it-py"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version >= '3.14' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.10.*'",
]
dependencies = [
{ name = "mdurl", marker = "python_full_version >= '3.10'" },
@@ -435,7 +471,16 @@ name = "platformdirs"
version = "4.10.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version >= '3.14' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.10.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" }
wheels = [
@@ -668,7 +713,16 @@ name = "pytest"
version = "9.1.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version >= '3.14' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.10.*'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
@@ -903,7 +957,16 @@ name = "typer"
version = "0.26.8"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version >= '3.14' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.10.*'",
]
dependencies = [
{ name = "annotated-doc", marker = "python_full_version >= '3.10'" },
+3 -1
View File
@@ -1,5 +1,7 @@
next_available_id: 1
next_available_ioc_port: 50003
services:
0:
000:
name: master
ioc_port: 50011
status: active
+1 -1
View File
@@ -15,7 +15,7 @@ ansible-playbook ansible/playbooks/add-new-service.yml \
```
ansible-playbook ansible/plays/ioc/ioc-install.yml \
-i ansible/hosts.yml \
--extra-vars="agebd_env=dev branch_name=feature/add-service-001-playground new_service_dir_name=001-playground" \
--extra-vars="agebd_env=dev branch_name=feature/add-service-001-playground new_service_dir_name=001-playground ioc_port=50003" \
-v
```
View File
+5
View File
@@ -0,0 +1,5 @@
# Overview of IOCs for Beam Dynamics
| IOC NAME | Port | Description |
|---|---|---|
| AGEBD-CPCL-MASTER | 50011 | IOC providing PVs for the service master application IOC enabling to monitor the health status of BD High Level Application Services with the alarm handler ALH |
@@ -1,6 +1,6 @@
cpu_architecture: x86_64
epics_version: 7.0.8
ioc_host: sls-vserv-bd-01{{ agebd_env }}
ioc_port: 50011
ioc_port: {{ ioc_port }}
os: RHEL8
os_id: rhel
@@ -87,8 +87,6 @@ class Service(BaseService[PVs]):
# get some pvs/vals
self.val1 = self.pvs.my_pv1.get()
self.val2 = self.pvs.my_pv2.get()
print(self.val1)
print(self.val2)
# maybe sleep to limit the update loop execution rate
sleep(1)
@@ -1,6 +1,6 @@
cpu_architecture: x86_64
epics_version: 7.0.8
ioc_host: sls-vserv-bd-01{{ '{{ agebd_env }}' }}
ioc_port: {{ ioc_port }}
ioc_port: {{ '{{ ioc_port }}' }}
os: RHEL8
os_id: rhel