laniakea-api-agent 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. laniakea_api_agent-0.0.1/.gitignore +9 -0
  2. laniakea_api_agent-0.0.1/PKG-INFO +89 -0
  3. laniakea_api_agent-0.0.1/README.md +56 -0
  4. laniakea_api_agent-0.0.1/laniakea-agent/__init__.py +4 -0
  5. laniakea_api_agent-0.0.1/laniakea-agent/ansible_agent.py +45 -0
  6. laniakea_api_agent-0.0.1/laniakea-agent/ansible_worker.py +116 -0
  7. laniakea_api_agent-0.0.1/laniakea-agent/api_client.py +156 -0
  8. laniakea_api_agent-0.0.1/laniakea-agent/auth_utils/__init__.py +0 -0
  9. laniakea_api_agent-0.0.1/laniakea-agent/auth_utils/aws_auth.py +27 -0
  10. laniakea_api_agent-0.0.1/laniakea-agent/auth_utils/openstack_auth.py +33 -0
  11. laniakea_api_agent-0.0.1/laniakea-agent/cli.py +97 -0
  12. laniakea_api_agent-0.0.1/laniakea-agent/destroy.py +123 -0
  13. laniakea_api_agent-0.0.1/laniakea-agent/notifier.py +125 -0
  14. laniakea_api_agent-0.0.1/laniakea-agent/repo_url_template.yml +9 -0
  15. laniakea_api_agent-0.0.1/laniakea-agent/terraform/aws/main.tf +81 -0
  16. laniakea_api_agent-0.0.1/laniakea-agent/terraform/aws/variables.tf +56 -0
  17. laniakea_api_agent-0.0.1/laniakea-agent/terraform/openstack_garr/main.tf +110 -0
  18. laniakea_api_agent-0.0.1/laniakea-agent/terraform/openstack_garr/variables.tf +83 -0
  19. laniakea_api_agent-0.0.1/laniakea-agent/terraform/openstack_recas/main.tf +104 -0
  20. laniakea_api_agent-0.0.1/laniakea-agent/terraform/openstack_recas/variables.tf +83 -0
  21. laniakea_api_agent-0.0.1/laniakea-agent/terraform_agent.py +407 -0
  22. laniakea_api_agent-0.0.1/laniakea-agent/vault_utils.py +76 -0
  23. laniakea_api_agent-0.0.1/laniakea-agent/worker_wrapper.py +7 -0
  24. laniakea_api_agent-0.0.1/pyproject.toml +58 -0
  25. laniakea_api_agent-0.0.1/worker_wrapper.py +13 -0
@@ -0,0 +1,9 @@
1
+ .env
2
+ *.key
3
+ keys/
4
+ *.crt
5
+ __pycache__/
6
+ *.pem
7
+ *.log
8
+ .terraform*
9
+ terraform.tfstate*
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: laniakea-api-agent
3
+ Version: 0.0.1
4
+ Summary: Laniakea deployment agent orchestrates cloud deployments via Terraform and Ansible
5
+ Project-URL: Homepage, https://github.com/riccardocaccia/laniakea-agent/tree/laniakea-agent-pkg
6
+ Project-URL: Issues, https://github.com/riccardocaccia/laniakea-agent/issues
7
+ Author: rcaccia
8
+ License: MIT
9
+ Keywords: ansible,aws,cloud,deployment,openstack,redis,terraform
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: System :: Systems Administration
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: docker
20
+ Requires-Dist: httpx
21
+ Requires-Dist: hvac>=2.0.0
22
+ Requires-Dist: keystoneauth1[openidconnect]
23
+ Requires-Dist: openstacksdk
24
+ Requires-Dist: pydantic>=2.0.0
25
+ Requires-Dist: pyjwt
26
+ Requires-Dist: python-dotenv
27
+ Requires-Dist: python-keystoneclient
28
+ Requires-Dist: pyyaml
29
+ Requires-Dist: redis
30
+ Requires-Dist: requests
31
+ Requires-Dist: rq
32
+ Description-Content-Type: text/markdown
33
+
34
+ # laniakea-api-agent
35
+
36
+ Cloud deployment worker agent for the Laniakea orchestration platform.
37
+ Consumes jobs from Redis queues and orchestrates VM provisioning via Terraform and Ansible.
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install laniakea-api-agent
43
+ ```
44
+
45
+ ## Configuration
46
+
47
+ Create a `.env` file in your working directory:
48
+
49
+ ```bash
50
+ # Redis
51
+ REDIS_HOST=your-redis-host
52
+ REDIS_PORT=6379
53
+ REDIS_PASSWORD=your-redis-password
54
+
55
+ # Laniakea API
56
+ LANIAKEA_API_URL=https://your-api-host:8443/laniakea_core/v1.0
57
+ AGENT_MASTER_PASSWORD=your-shared-secret
58
+ AGENT_ID=laniakea-agent-1
59
+
60
+ # Vault
61
+ VAULT_ADDR=https://your-vault:8200
62
+ VAULT_TOKEN=your-vault-token
63
+
64
+ # Logs
65
+ DEPLOYMENT_LOG_DIR=/var/log/laniakea-agent
66
+ ```
67
+
68
+ ## Usage
69
+
70
+ ```bash
71
+ # listen on openstack queue (default)
72
+ laniakea-agent
73
+
74
+ # listen on a specific queue
75
+ laniakea-agent --queue openstack
76
+
77
+ # listen on multiple queues
78
+ laniakea-agent --queue openstack --queue aws
79
+
80
+ # use a custom .env file
81
+ laniakea-agent --env /etc/laniakea/agent.env
82
+ ```
83
+
84
+ ## Requirements
85
+
86
+ - Python 3.10+
87
+ - Docker (for Terraform containers)
88
+ - Ansible installed on the host
89
+
@@ -0,0 +1,56 @@
1
+ # laniakea-api-agent
2
+
3
+ Cloud deployment worker agent for the Laniakea orchestration platform.
4
+ Consumes jobs from Redis queues and orchestrates VM provisioning via Terraform and Ansible.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install laniakea-api-agent
10
+ ```
11
+
12
+ ## Configuration
13
+
14
+ Create a `.env` file in your working directory:
15
+
16
+ ```bash
17
+ # Redis
18
+ REDIS_HOST=your-redis-host
19
+ REDIS_PORT=6379
20
+ REDIS_PASSWORD=your-redis-password
21
+
22
+ # Laniakea API
23
+ LANIAKEA_API_URL=https://your-api-host:8443/laniakea_core/v1.0
24
+ AGENT_MASTER_PASSWORD=your-shared-secret
25
+ AGENT_ID=laniakea-agent-1
26
+
27
+ # Vault
28
+ VAULT_ADDR=https://your-vault:8200
29
+ VAULT_TOKEN=your-vault-token
30
+
31
+ # Logs
32
+ DEPLOYMENT_LOG_DIR=/var/log/laniakea-agent
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ```bash
38
+ # listen on openstack queue (default)
39
+ laniakea-agent
40
+
41
+ # listen on a specific queue
42
+ laniakea-agent --queue openstack
43
+
44
+ # listen on multiple queues
45
+ laniakea-agent --queue openstack --queue aws
46
+
47
+ # use a custom .env file
48
+ laniakea-agent --env /etc/laniakea/agent.env
49
+ ```
50
+
51
+ ## Requirements
52
+
53
+ - Python 3.10+
54
+ - Docker (for Terraform containers)
55
+ - Ansible installed on the host
56
+
@@ -0,0 +1,4 @@
1
+ """
2
+ Consumes jobs from Redis queues and orchestrates Terraform + Ansible deployments.
3
+ """
4
+ __version__ = "0.1.0"
@@ -0,0 +1,45 @@
1
+ import os
2
+ import stat
3
+ import logging
4
+ from laniakea_agent.ansible_worker import AnsibleWorker
5
+
6
+ # create a logging istance (for the ansible_agent -> __name__) used for the debugging
7
+ logger = logging.getLogger(__name__)
8
+
9
+ def run_ansible_step(job, playbook_url, requirements_url):
10
+ """
11
+ Passes the needed info to the ansible worker funcions in order to
12
+ configure the machine with the correct software.
13
+ """
14
+ uuid = job.deployment_uuid
15
+ # NOTE: PRIVATE KEY MANAGEMENT to be imporved
16
+ key_path = f"keys/{uuid}.pem"
17
+
18
+ worker = AnsibleWorker(playbook_url, requirements_url, uuid)
19
+
20
+ try:
21
+ success_prep, msg = worker.prepare_environment()
22
+
23
+ if not success_prep:
24
+ logger.error(f"[{uuid}] ERROR in Ansible preparation: {msg}")
25
+ return False
26
+
27
+ # bastion identification
28
+ bastion = "0.0.0.0"
29
+ if job.selected_provider.lower() == 'openstack':
30
+ os_data = job.cloud_providers.openstack
31
+ if os_data.inputs.network_type == 'private':
32
+ bastion = os_data.private_network_proxy_host or "0.0.0.0"
33
+
34
+ success= worker.execute_deployment(
35
+ target_ip=job.vm_ip,
36
+ ssh_key_path=key_path,
37
+ bastion_ip=bastion
38
+ )
39
+ worker.cleanup() # clean /tmp/uuid
40
+ return success
41
+
42
+ except Exception as e:
43
+ logger.error(f"[{uuid}] EXCEPTION in an Ansible step: {e}")
44
+ worker.cleanup()
45
+ return False
@@ -0,0 +1,116 @@
1
+ import os
2
+ import subprocess
3
+ import shutil
4
+ import requests
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ # NOTE: move from here, not interesting... also update from rcaccia to laniakea
10
+ GROUP_VARS_URL = "https://raw.githubusercontent.com/riccardocaccia/laniakea-nebula/clean-main/terraform/ansible/group_vars/galaxy.yml"
11
+ NGINX_TEMPLATE_URL = "https://raw.githubusercontent.com/riccardocaccia/laniakea-nebula/clean-main/terraform/ansible/templates/nginx/galaxy.j2"
12
+ ANSIBLE_VENV = "/tmp/ansible-venv"
13
+
14
+ class AnsibleWorker:
15
+ def __init__(self, playbook_url, requirements_url, uuid):
16
+ self.playbook_url = playbook_url
17
+ self.requirements_url = requirements_url
18
+ self.uuid = uuid
19
+ self.base_dir = f"/tmp/{uuid}"
20
+ self.playbook_path = os.path.join(self.base_dir, "deploy.yml")
21
+ self.requirements_path = os.path.join(self.base_dir, "requirements.yml")
22
+ self.group_vars_path = os.path.join(self.base_dir, "group_vars", "all.yml")
23
+ self.nginx_template_path = os.path.join(self.base_dir, "templates", "nginx", "galaxy.j2")
24
+
25
+ def prepare_environment(self):
26
+ try:
27
+ os.makedirs(self.base_dir, exist_ok=True)
28
+ os.makedirs(os.path.join(self.base_dir, "group_vars"), exist_ok=True)
29
+ os.makedirs(os.path.join(self.base_dir, "templates", "nginx"), exist_ok=True)
30
+
31
+ with open(self.playbook_path, "wb") as f:
32
+ f.write(requests.get(self.playbook_url).content)
33
+ with open(self.requirements_path, "wb") as f:
34
+ f.write(requests.get(self.requirements_url).content)
35
+ with open(self.group_vars_path, "wb") as f:
36
+ f.write(requests.get(GROUP_VARS_URL).content)
37
+ with open(self.nginx_template_path, "wb") as f:
38
+ f.write(requests.get(NGINX_TEMPLATE_URL).content)
39
+
40
+ return True, "OK"
41
+ except Exception as e:
42
+ return False, str(e)
43
+
44
+ def execute_deployment(self, target_ip, ssh_key_path, bastion_ip=None):
45
+ """
46
+ Run CLI command to copy & download playbooks.
47
+ Make a proxy jump if a bastion is specified.
48
+ """
49
+ ssh_base = (
50
+ f"ssh -i {ssh_key_path} "
51
+ f"-o StrictHostKeyChecking=no "
52
+ f"-o UserKnownHostsFile=/dev/null "
53
+ )
54
+ scp_base = (
55
+ f"scp -i {ssh_key_path} "
56
+ f"-o StrictHostKeyChecking=no "
57
+ f"-o UserKnownHostsFile=/dev/null "
58
+ )
59
+
60
+ if bastion_ip and bastion_ip != "0.0.0.0":
61
+ proxy = (
62
+ f"-o ProxyCommand='ssh -i {ssh_key_path} "
63
+ f"-o StrictHostKeyChecking=no -W %h:%p rocky@{bastion_ip}'"
64
+ )
65
+ ssh_base += proxy + " "
66
+ scp_base += proxy + " "
67
+
68
+ remote = f"rocky@{target_ip}"
69
+
70
+ steps = [
71
+ # 1. Create directory strucfure VM
72
+ f"{ssh_base} {remote} 'sudo mkdir -p /tmp/galaxy-deploy/group_vars /tmp/galaxy-deploy/templates/nginx && sudo chown -R rocky /tmp/galaxy-deploy'",
73
+
74
+ # 2. Copy all files
75
+ f"{scp_base} {self.playbook_path} {remote}:/tmp/galaxy-deploy/deploy.yml",
76
+ f"{scp_base} {self.requirements_path} {remote}:/tmp/galaxy-deploy/requirements.yml",
77
+ f"{scp_base} {self.group_vars_path} {remote}:/tmp/galaxy-deploy/group_vars/all.yml",
78
+ f"{scp_base} {self.nginx_template_path} {remote}:/tmp/galaxy-deploy/templates/nginx/galaxy.j2",
79
+
80
+ # 3. Create a virtualenv and install ansible
81
+ f"{ssh_base} {remote} 'sudo dnf install -y python3-pip git && sudo python3 -m venv {ANSIBLE_VENV} && sudo {ANSIBLE_VENV}/bin/pip install ansible \"virtualenv<20.22\"'",
82
+
83
+ # 4. Install ansible roles
84
+ f"{ssh_base} {remote} 'sudo {ANSIBLE_VENV}/bin/ansible-galaxy install -r /tmp/galaxy-deploy/requirements.yml -p /tmp/galaxy-deploy/roles'",
85
+
86
+ # 5. execute playbooks in local as root
87
+ (
88
+ f"{ssh_base} {remote} "
89
+ f"'cd /tmp/galaxy-deploy && "
90
+ f"sudo ANSIBLE_ROLES_PATH=/tmp/galaxy-deploy/roles "
91
+ f"{ANSIBLE_VENV}/bin/ansible-playbook "
92
+ f"-i localhost, -c local "
93
+ f"-e \"target_hosts=localhost\" "
94
+ f"/tmp/galaxy-deploy/deploy.yml "
95
+ f"2>&1 | sudo tee /tmp/galaxy-deploy/ansible.log; "
96
+ f"exit ${{PIPESTATUS[0]}}'"
97
+ ),
98
+ ]
99
+
100
+ # debug helper
101
+ for i, step in enumerate(steps):
102
+ logger.info(f"[{self.uuid}] Step {i+1}/{len(steps)}: {step[:80]}...")
103
+ res = subprocess.run(step, shell=True)
104
+ if res.returncode != 0:
105
+ logger.error(f"[{self.uuid}] Step {i+1} fallito.")
106
+ return False
107
+
108
+ return True
109
+
110
+ def cleanup(self):
111
+ """
112
+ Clean all the directories after the process
113
+ """
114
+ if os.path.exists(self.base_dir):
115
+ shutil.rmtree(self.base_dir)
116
+ logger.info(f"[{self.uuid}] Pulizia cartella temporanea completata.")
@@ -0,0 +1,156 @@
1
+ """
2
+ Calls the Laniakea Queue API to update deployment status.
3
+ Auth: JWT signed with AGENT_MASTER_PASSWORD (HMAC-SHA256).
4
+ No certificates needed just the shared master password.
5
+
6
+ pool password model:
7
+ - one master password governs all agents
8
+ - to revoke ALL agents: change the password on API + all agents and restart
9
+ """
10
+
11
+ import jwt
12
+ import logging
13
+ import os
14
+ import time
15
+ import uuid
16
+ from typing import Optional
17
+ import httpx
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Base URL of the Laniakea queue API (HTTPS default port)
22
+ API_BASE_URL = os.getenv("LANIAKEA_API_URL", "https://.......:8443")
23
+ AGENT_MASTER_PASSWORD = os.getenv("AGENT_MASTER_PASSWORD", "")
24
+ AGENT_ID = os.getenv("AGENT_ID", "laniakea-agent")
25
+
26
+ # NOTE: CA cert to verify the API server's TLS certificate
27
+ AGENT_CA_CERT = os.getenv("AGENT_CA_CERT", "certs/ca.crt")
28
+
29
+ # token lifespan
30
+ TOKEN_TTL_SECONDS = 300 # short lived token
31
+
32
+ # Token generation
33
+ def _mint_token() -> str:
34
+ """
35
+ Generate a short-lived JWT signed with the master password.
36
+
37
+ Payload:
38
+ sub: agent identity (AGENT_ID from .env)
39
+ iat: issued at
40
+ exp: expires in TOKEN_TTL_SECONDS
41
+ jti: unique token ID
42
+ """
43
+ if not AGENT_MASTER_PASSWORD:
44
+ raise RuntimeError("AGENT_MASTER_PASSWORD is not set.")
45
+
46
+ now = int(time.time())
47
+ payload = {
48
+ "sub": AGENT_ID,
49
+ "iat": now,
50
+ "exp": now + TOKEN_TTL_SECONDS,
51
+ "jti": str(uuid.uuid4()),
52
+ }
53
+ return jwt.encode(payload, AGENT_MASTER_PASSWORD, algorithm="HS256")
54
+
55
+
56
+ # Internal helper
57
+ def _make_client() -> httpx.Client:
58
+ """
59
+ Build an httpx Client with:
60
+ - Authorization: Bearer <JWT> for agent authentication
61
+ - TLS server verification via CA cert
62
+ """
63
+ token = _mint_token()
64
+
65
+ # Use the CA cert if it exists, otherwise fall back to system bundle.
66
+ # The CA cert here verifies the API SERVER certificate — not client auth.
67
+ verify: str | bool = AGENT_CA_CERT if os.path.exists(AGENT_CA_CERT) else True
68
+
69
+ return httpx.Client(
70
+ base_url=API_BASE_URL,
71
+ headers={"Authorization": f"Bearer {token}"},
72
+ verify=verify,
73
+ timeout=30,
74
+ )
75
+
76
+
77
+ # Public interface called by terraform_agent.py
78
+
79
+ def update_deployment_status(
80
+ deployment_uuid: str, new_status: str, status_reason: Optional[str] = None, outputs: Optional[str] = None,)-> bool:
81
+ # NOTE: add email, not every time only the first
82
+ """
83
+ PATCH /internal/deployments/{uuid}/status on the queue API.
84
+
85
+ Parameters
86
+ ----------
87
+ deployment_uuid : str
88
+ UUID of the deployment to update.
89
+ new_status : str
90
+ One of: CREATE_IN_PROGRESS, CREATE_COMPLETE, CREATE_FAILED,
91
+ UPDATE_IN_PROGRESS, UPDATE_FAILED.
92
+ status_reason : str, optional
93
+ Human-readable reason (used on FAILED states).
94
+ outputs : str, optional
95
+ JSON string with deployment outputs (e.g. vm_ip).
96
+
97
+ Returns
98
+ -------
99
+ bool
100
+ True on success, False if the API rejected or was unreachable.
101
+ The caller decides whether to raise or continue.
102
+ """
103
+ payload = {"status": new_status.upper()}
104
+ if status_reason:
105
+ payload["status_reason"] = status_reason
106
+ if outputs:
107
+ payload["outputs"] = outputs
108
+
109
+ try:
110
+ with _make_client() as client:
111
+ response = client.patch(
112
+ f"/internal/deployments/{deployment_uuid}/status",
113
+ json=payload,
114
+ )
115
+
116
+ if response.status_code == 200:
117
+ logger.info("[%s] Status updated to %s via API.", deployment_uuid, new_status)
118
+ return True
119
+
120
+ # 409 = invalid transition (e.g. agent tried to go COMPLETE -> IN_PROGRESS)
121
+ # 404 = deployment not found in DB (API never saw the QUEUED write)
122
+ logger.error(
123
+ "[%s] API rejected status update to %s: HTTP %s — %s",
124
+ deployment_uuid,
125
+ new_status,
126
+ response.status_code,
127
+ response.text,
128
+ )
129
+ return False
130
+
131
+ except httpx.RequestError as exc:
132
+ logger.error(
133
+ "[%s] Could not reach API to update status: %s", deployment_uuid, exc
134
+ )
135
+ return False
136
+
137
+ def push_log_line(deployment_uuid: str, level: str, message: str) -> None:
138
+ """
139
+ POST /internal/deployments/{uuid}/logs on the Queue API.
140
+
141
+ Sends a single formatted log line so the API can accumulate it in
142
+ logs/orchestrator-{uuid}.log on the API VM. The dashboard reads that
143
+ file via GET /api/deployments/{uuid}/logs.
144
+
145
+ Failures are silently swallowed.
146
+ """
147
+ payload = {
148
+ "level": level.upper(),
149
+ "message": message,
150
+ }
151
+ try:
152
+ with _make_client() as client:
153
+ client.post(f"/internal/deployments/{deployment_uuid}/logs", json=payload,)
154
+ except Exception:
155
+ pass # never crash the agent because of a log push failure
156
+
@@ -0,0 +1,27 @@
1
+ from vault_utils import get_secrets
2
+ import logging
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+ def get_aws_credentials(group: str):
7
+ """
8
+ AWS keys retrieving from Vault.
9
+ """
10
+ try:
11
+ # TODO: modify once the final vault is up
12
+ vault_path = f"SECRET/infrastructure/aws/{group}"
13
+ secrets = get_secrets(vault_path)
14
+
15
+ if not secrets:
16
+ # NOTE : TEST!!! remove default implementation
17
+ logger.warning(f"No secret found for the group: {group}, TRYING DEFAULT...")
18
+ secrets = get_secrets("infrastructure/aws/default")
19
+
20
+ return {
21
+ "access": secrets.get("access_key"),
22
+ "secret": secrets.get("secret_key")
23
+ }
24
+
25
+ except Exception as e:
26
+ logger.error(f"Error in AWS credential retrieving from Vault: {e}")
27
+ return None
@@ -0,0 +1,33 @@
1
+ from keystoneauth1 import loading
2
+ from keystoneauth1 import session
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ def get_openstack_admin_creds():
8
+ ''' access to retrieve OpenStack credentials from Vault '''
9
+ # CHANGE THE VAULT PATH ONCE VAULT BUILD FINAL COMPLETED
10
+ return get_secrets("SECRET/infrastructure/openstack/admin")
11
+
12
+ def get_keystone_token(aai_token, auth_url, project_id):
13
+ try:
14
+ loader = loading.get_plugin_loader('v3oidcaccesstoken')
15
+
16
+ auth = loader.load_from_options(
17
+ auth_url=auth_url,
18
+ ### TODO: env variable? or input? ###
19
+ identity_provider='recas-bari',
20
+ #####################################
21
+ protocol='openid',
22
+ access_token=aai_token,
23
+ project_id=project_id
24
+ )
25
+
26
+ sess = session.Session(auth=auth, verify=True)
27
+ token_os = sess.get_token()
28
+ logger.info("Token AAI exchange: success!")
29
+ return token_os
30
+
31
+ except Exception as e:
32
+ logger.error(f"ERROR [OpenStack Auth]: {e}")
33
+ return None
@@ -0,0 +1,97 @@
1
+ """
2
+ Entry point for laniakea-agent.
3
+
4
+ After running `pip install laniakea-agent`, the user can launch it with:
5
+ laniakea-agent --queue openstack
6
+ laniakea-agent --queue aws
7
+ laniakea-agent --queue openstack --queue aws # listens to both
8
+
9
+ The process loads the .env file from the current directory, connects to Redis,
10
+ and spawns the RQ Worker listening on the specified queues.
11
+
12
+ RQ imports 'worker_wrapper.run_from_dict' by its string name, this works
13
+ because laniakea_agent is an installed package and worker_wrapper is accessible
14
+ as laniakea_agent.worker_wrapper. However, a local worker_wrapper in the working
15
+ directory (if present) takes precedence, allowing for overrides without
16
+ reinstalling the package.
17
+ """
18
+
19
+ import argparse
20
+ import os
21
+ import sys
22
+
23
+
24
+ def main():
25
+ parser = argparse.ArgumentParser(
26
+ prog="laniakea-agent",
27
+ description="Laniakea deployment agent which consumes jobs from Redis queues.",
28
+ )
29
+ parser.add_argument(
30
+ "--queue", "-q",
31
+ action="append",
32
+ dest="queues",
33
+ default=None,
34
+ metavar="NAME",
35
+ help="Queue name to listen on (e.g. openstack, aws). Repeat for multiple queues.",
36
+ )
37
+ parser.add_argument(
38
+ "--env", "-e",
39
+ default=".env",
40
+ metavar="FILE",
41
+ help="Path to .env file (default: .env in current directory).",
42
+ )
43
+ parser.add_argument(
44
+ "--version", "-v",
45
+ action="store_true",
46
+ help="Print version and exit.",
47
+ )
48
+ args = parser.parse_args()
49
+
50
+ if args.version:
51
+ from laniakea_agent import __version__
52
+ print(f"laniakea-agent {__version__}")
53
+ sys.exit(0)
54
+
55
+ # load .env
56
+ env_path = os.path.abspath(args.env)
57
+ if os.path.exists(env_path):
58
+ from dotenv import load_dotenv
59
+ load_dotenv(env_path)
60
+ print(f"[config] loaded {env_path}")
61
+ else:
62
+ print(f"[config] .env not found at {env_path} — using environment variables")
63
+
64
+ # validate required env vars
65
+ missing = [v for v in ["REDIS_HOST", "REDIS_PASSWORD", "AGENT_MASTER_PASSWORD", "LANIAKEA_API_URL"]
66
+ if not os.getenv(v)]
67
+ if missing:
68
+ print(f"[error] missing required environment variables: {', '.join(missing)}")
69
+ print(" set them in .env or export them before running laniakea-agent")
70
+ sys.exit(1)
71
+
72
+ # default queue
73
+ queue_names = args.queues or ["openstack"]
74
+
75
+ # connect Redis
76
+ from redis import Redis
77
+ from rq import Worker, Queue
78
+
79
+ redis_conn = Redis(
80
+ host=os.getenv("REDIS_HOST"),
81
+ port=int(os.getenv("REDIS_PORT", "6379")),
82
+ password=os.getenv("REDIS_PASSWORD"),
83
+ decode_responses=False,
84
+ )
85
+
86
+ queues = [Queue(name, connection=redis_conn) for name in queue_names]
87
+
88
+ print(f"[agent] laniakea-agent listening on queues: {queue_names}")
89
+ print(f"[agent] redis: {os.getenv('REDIS_HOST')}:{os.getenv('REDIS_PORT', '6379')}")
90
+ print(f"[agent] api: {os.getenv('LANIAKEA_API_URL')}")
91
+ print(f"[agent] id: {os.getenv('AGENT_ID', 'laniakea-agent')}")
92
+
93
+ Worker(queues, connection=redis_conn).work()
94
+
95
+ if __name__ == "__main__":
96
+ main()
97
+