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.
- laniakea_api_agent-0.0.1/.gitignore +9 -0
- laniakea_api_agent-0.0.1/PKG-INFO +89 -0
- laniakea_api_agent-0.0.1/README.md +56 -0
- laniakea_api_agent-0.0.1/laniakea-agent/__init__.py +4 -0
- laniakea_api_agent-0.0.1/laniakea-agent/ansible_agent.py +45 -0
- laniakea_api_agent-0.0.1/laniakea-agent/ansible_worker.py +116 -0
- laniakea_api_agent-0.0.1/laniakea-agent/api_client.py +156 -0
- laniakea_api_agent-0.0.1/laniakea-agent/auth_utils/__init__.py +0 -0
- laniakea_api_agent-0.0.1/laniakea-agent/auth_utils/aws_auth.py +27 -0
- laniakea_api_agent-0.0.1/laniakea-agent/auth_utils/openstack_auth.py +33 -0
- laniakea_api_agent-0.0.1/laniakea-agent/cli.py +97 -0
- laniakea_api_agent-0.0.1/laniakea-agent/destroy.py +123 -0
- laniakea_api_agent-0.0.1/laniakea-agent/notifier.py +125 -0
- laniakea_api_agent-0.0.1/laniakea-agent/repo_url_template.yml +9 -0
- laniakea_api_agent-0.0.1/laniakea-agent/terraform/aws/main.tf +81 -0
- laniakea_api_agent-0.0.1/laniakea-agent/terraform/aws/variables.tf +56 -0
- laniakea_api_agent-0.0.1/laniakea-agent/terraform/openstack_garr/main.tf +110 -0
- laniakea_api_agent-0.0.1/laniakea-agent/terraform/openstack_garr/variables.tf +83 -0
- laniakea_api_agent-0.0.1/laniakea-agent/terraform/openstack_recas/main.tf +104 -0
- laniakea_api_agent-0.0.1/laniakea-agent/terraform/openstack_recas/variables.tf +83 -0
- laniakea_api_agent-0.0.1/laniakea-agent/terraform_agent.py +407 -0
- laniakea_api_agent-0.0.1/laniakea-agent/vault_utils.py +76 -0
- laniakea_api_agent-0.0.1/laniakea-agent/worker_wrapper.py +7 -0
- laniakea_api_agent-0.0.1/pyproject.toml +58 -0
- laniakea_api_agent-0.0.1/worker_wrapper.py +13 -0
|
@@ -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,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
|
+
|
|
File without changes
|
|
@@ -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
|
+
|