laniakea-api-server 0.1.2__tar.gz → 0.2.0__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 (24) hide show
  1. laniakea_api_server-0.2.0/LICENSE +21 -0
  2. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/PKG-INFO +3 -2
  3. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/__init__.py +1 -1
  4. laniakea_api_server-0.2.0/laniakea_api/auth.py +134 -0
  5. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/cli.py +16 -6
  6. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/config.py +2 -2
  7. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/credential_parser.py +9 -13
  8. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/database.py +9 -1
  9. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/installer.py +27 -5
  10. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/main.py +9 -5
  11. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/models.py +6 -3
  12. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/routers/agent.py +1 -1
  13. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/routers/agents.py +2 -1
  14. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/pyproject.toml +2 -2
  15. laniakea_api_server-0.1.2/laniakea_api/auth.py +0 -103
  16. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/.github/workflows/publish.yml +0 -0
  17. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/.gitignore +0 -0
  18. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/README.md +0 -0
  19. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/queue.py +0 -0
  20. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/routers/.health.py.swp +0 -0
  21. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/routers/__init__.py +0 -0
  22. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/routers/credentials.py +0 -0
  23. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/routers/deployments.py +0 -0
  24. {laniakea_api_server-0.1.2 → laniakea_api_server-0.2.0}/laniakea_api/routers/health.py +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Riccardo Caccia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: laniakea-api-server
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Laniakea Queue API — OIDC-authenticated gateway for cloud deployment orchestration
5
5
  Project-URL: Homepage, https://github.com/laniakea/laniakea-api
6
6
  Project-URL: Issues, https://github.com/laniakea/laniakea-api/issues
7
- Author: Laniakea Team
7
+ Author: Riccardo Caccia
8
8
  License: MIT
9
+ License-File: LICENSE
9
10
  Keywords: aws,cloud,deployment,fastapi,openstack,orchestration,redis
10
11
  Classifier: Development Status :: 3 - Alpha
11
12
  Classifier: Framework :: FastAPI
@@ -2,4 +2,4 @@
2
2
  OIDC-authenticated gateway for enqueuing cloud deployment jobs.
3
3
  """
4
4
 
5
- __version__ = "0.0.1"
5
+ __version__ = "0.1.4"
@@ -0,0 +1,134 @@
1
+ """
2
+ Authentication helpers
3
+
4
+ It is responsible for verifying the identity of those who knock on
5
+ the server's door distinguishing between two types of users:
6
+ human users (who pass through an OIDC Sign-in system) and worker agents
7
+ """
8
+ import time
9
+ import httpx
10
+ import jwt
11
+ from fastapi import Depends, HTTPException, status
12
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
13
+ from laniakea_api.config import (
14
+ SECRET_KEY, ALGORITHM, SESSION_TTL_MINUTES,
15
+ OIDC_DISCOVERY_URL, AGENT_MASTER_PASSWORD,
16
+ )
17
+
18
+ # Initialize security OAuth2 Bearer Token
19
+ _bearer = HTTPBearer()
20
+
21
+ async def fetch_userinfo(oidc_token: str) -> dict:
22
+ """
23
+ Takes an authentication token provided by a user and asks an external identity server
24
+ (the OIDC Provider) who that user actually is, retriving user info
25
+ """
26
+ try:
27
+ # asynchronous HTTP to avoid waste of server resources
28
+ async with httpx.AsyncClient(timeout=10) as client:
29
+ # OIDC discovery
30
+ discovery = await client.get(OIDC_DISCOVERY_URL)
31
+ discovery.raise_for_status() # status 200: OK
32
+ userinfo_url = discovery.json()["userinfo_endpoint"] # now final url is known
33
+ # USER info request
34
+ resp = await client.get(
35
+ userinfo_url,
36
+ headers={"Authorization": f"Bearer {oidc_token}"},
37
+ )
38
+ if resp.status_code != 200:
39
+ raise HTTPException(
40
+ status_code=status.HTTP_401_UNAUTHORIZED,
41
+ detail=f"OIDC userinfo returned {resp.status_code}",
42
+ )
43
+ return resp.json()
44
+ except HTTPException:
45
+ raise
46
+ except Exception as exc:
47
+ raise HTTPException(
48
+ status_code=status.HTTP_401_UNAUTHORIZED,
49
+ detail=f"OIDC validation failed: {exc}",
50
+ )
51
+
52
+
53
+ def create_session_token(user_info: dict) -> tuple:
54
+ """
55
+ Querying the external OIDC server for every single request would slow down the API.
56
+ To avoid this behaviour, once the user has been verified by fetch_userinfo, this function
57
+ generates an internal JWT token:
58
+
59
+ It takes the user's data (sub, username, email, groups).
60
+ It adds an expiration date based on the minutes configured in SESSION_TTL_MINUTES.
61
+ It cryptographically signs everything with a secret key (SECRET_KEY)
62
+ It returns a compact token that the user will use for subsequent requests.
63
+ """
64
+ #NOTE: act here for expiration
65
+ expires_in = SESSION_TTL_MINUTES * 60
66
+ now = int(time.time())
67
+ #NOTE: PAYLOAD
68
+ payload = {
69
+ "sub": user_info.get("sub"),
70
+ "username": user_info.get("preferred_username") or user_info.get("sub"),
71
+ "email": user_info.get("email"),
72
+ "groups": user_info.get("groups", []),
73
+ "iat": now,
74
+ "exp": now + expires_in,
75
+ }
76
+ return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM), expires_in
77
+
78
+
79
+ async def verify_session_token(credentials: HTTPAuthorizationCredentials = Depends(_bearer),) -> dict:
80
+ """
81
+ Check that the HTTP request contains an authorization header.
82
+ Takes the internal JWT token created in the previous step and checks
83
+ whether the signature is authentic (using the SECRET_KEY).
84
+ """
85
+ try:
86
+ return jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
87
+
88
+ except jwt.ExpiredSignatureError:
89
+ raise HTTPException(
90
+ status_code=status.HTTP_401_UNAUTHORIZED,
91
+ detail="Session token expired",
92
+ )
93
+
94
+ except jwt.InvalidTokenError as exc:
95
+ raise HTTPException(
96
+ status_code=status.HTTP_401_UNAUTHORIZED,
97
+ detail=f"Invalid session token: {exc}",
98
+ )
99
+
100
+
101
+ async def verify_agent_token(credentials: HTTPAuthorizationCredentials = Depends(_bearer),) -> str:
102
+ """
103
+ This function validates the tokens used by workers.
104
+ It doesn't use the user's key, but a dedicated secret key
105
+ called AGENT_MASTER_PASSWORD.
106
+ If the worker sends a valid token signed with this master password,
107
+ the API trusts the worker and allows it to fetch or update the status of deployment jobs.
108
+
109
+ HTCondor pool-password like.
110
+ """
111
+ if not AGENT_MASTER_PASSWORD:
112
+ raise HTTPException(
113
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
114
+ detail="AGENT_MASTER_PASSWORD not configured, set it as an ambient variable or in the .env",
115
+ )
116
+ try:
117
+ payload = jwt.decode(
118
+ credentials.credentials,
119
+ AGENT_MASTER_PASSWORD,
120
+ algorithms=["HS256"],
121
+ )
122
+ # NOTE: unknown-agent default value
123
+ return payload.get("sub", "unknown-agent")
124
+
125
+ except jwt.ExpiredSignatureError:
126
+ raise HTTPException(
127
+ status_code=status.HTTP_401_UNAUTHORIZED,
128
+ detail="Agent token expired.",
129
+ )
130
+ except jwt.InvalidTokenError as exc:
131
+ raise HTTPException(
132
+ status_code=status.HTTP_401_UNAUTHORIZED,
133
+ detail=f"Invalid agent token: {exc}",
134
+ )
@@ -1,5 +1,5 @@
1
1
  """
2
- after pip install laniakea-api-server, the user throw:
2
+ after pip install laniakea-api-server, the user can throw:
3
3
  laniakea-api
4
4
  laniakea-api --port 8000 # HTTP, no TLS (test)
5
5
  laniakea-api --port 8443 --ssl # HTTPS con TLS
@@ -12,14 +12,19 @@ import sys
12
12
 
13
13
 
14
14
  def main():
15
+ """
16
+ Which option is accepted in the cli
17
+ """
15
18
  parser = argparse.ArgumentParser(
16
19
  prog="laniakea-api",
17
20
  description="Laniakea Queue API server.",
18
21
  )
22
+
19
23
  parser.add_argument("--host", default="0.0.0.0", help="Bind host (default: 0.0.0.0)")
20
24
  parser.add_argument("--port", default=8443, type=int, help="Bind port (default: 8443)")
21
25
  parser.add_argument("--env", default=".env", metavar="FILE", help="Path to .env file")
22
26
  parser.add_argument("--ssl", action="store_true", help="Enable TLS (requires SSL_KEYFILE and SSL_CERTFILE in .env)")
27
+ #FIXME: reload to be mantained?
23
28
  parser.add_argument("--reload", action="store_true", help="Enable auto-reload (development only)")
24
29
  parser.add_argument("--version", action="store_true", help="Print version and exit.")
25
30
  args = parser.parse_args()
@@ -30,15 +35,17 @@ def main():
30
35
  sys.exit(0)
31
36
 
32
37
  # load .env
38
+ # NOTE: if .env not specified local ambient variable are used
33
39
  env_path = os.path.abspath(args.env)
34
40
  if os.path.exists(env_path):
35
41
  from dotenv import load_dotenv
36
42
  load_dotenv(env_path)
37
43
  print(f"[config] loaded {env_path}")
38
44
  else:
39
- print(f"[config] .env not found at {env_path} using environment variables")
45
+ print(f"[config] .env not found at {env_path}... using environment variables")
40
46
 
41
47
  # validate required env vars
48
+ # NOTE: Check if all required vars
42
49
  required = [
43
50
  "SECRET_KEY", "AGENT_MASTER_PASSWORD",
44
51
  "REDIS_HOST", "REDIS_PASSWORD",
@@ -48,24 +55,26 @@ def main():
48
55
  ]
49
56
  missing = [v for v in required if not os.getenv(v)]
50
57
  if missing:
51
- print(f"[error] missing required environment variables: {', '.join(missing)}")
58
+ print(f"[error] missing required variables: {', '.join(missing)}")
52
59
  sys.exit(1)
53
60
 
54
- # create log dir if needed
61
+ # FIXME: check if the destination for this dir is ok
62
+ # create a log dir
55
63
  log_dir = os.getenv("DEPLOYMENT_LOG_DIR", "/var/log/laniakea-agent")
56
64
  os.makedirs(log_dir, exist_ok=True)
57
65
 
58
66
  import uvicorn
59
67
 
60
68
  uvicorn_kwargs = dict(
61
- app="laniakea_api.main:app",
69
+ app="laniakea_api.main:app", # App instance in the main file
62
70
  host=args.host,
63
71
  port=args.port,
64
72
  log_level="info",
65
- reload=args.reload,
73
+ reload=args.reload, # NOTE: To be removed if argparse removed
66
74
  )
67
75
 
68
76
  if args.ssl:
77
+ # if ssl option specified searches for SSL certificates and private key
69
78
  ssl_key = os.getenv("SSL_KEYFILE", "certs/api.key")
70
79
  ssl_cert = os.getenv("SSL_CERTFILE", "certs/api.crt")
71
80
  if not os.path.exists(ssl_key) or not os.path.exists(ssl_cert):
@@ -77,6 +86,7 @@ def main():
77
86
  else:
78
87
  print(f"[api] HTTP on {args.host}:{args.port} (no TLS)")
79
88
 
89
+ # Starting the application
80
90
  uvicorn.run(**uvicorn_kwargs)
81
91
 
82
92
 
@@ -1,5 +1,5 @@
1
1
  """
2
- Import the .env
2
+ Import the .env vars
3
3
  Inizialize all the service: Redis, Vault, Posgre
4
4
  """
5
5
 
@@ -17,7 +17,7 @@ AGENT_MASTER_PASSWORD = os.getenv("AGENT_MASTER_PASSWORD", "")
17
17
 
18
18
  # Redis
19
19
  REDIS_HOST = os.getenv("REDIS_HOST", "")
20
- # NOTE: Idk port
20
+ # NOTE: mod. port
21
21
  REDIS_PORT = int(os.getenv("REDIS_PORT", "1908"))
22
22
  REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "")
23
23
 
@@ -27,7 +27,8 @@ except ImportError:
27
27
  HAS_YAML = False
28
28
 
29
29
  # RC file parser
30
- # Maps RC env var names needed for internal credential field names
30
+ # This dictionary is used to translate the names of standard OpenStack variables (left)
31
+ # into the internal names used by the Laniakea database and APIs (right).
31
32
  _RC_FIELD_MAP = {
32
33
  "OS_AUTH_URL": "openstack_auth_url",
33
34
  "OS_APPLICATION_CREDENTIAL_ID": "openstack_app_credential_id",
@@ -44,7 +45,9 @@ def _parse_rc_file(path: Path) -> dict:
44
45
  export OS_APPLICATION_CREDENTIAL_ID=abc123
45
46
  """
46
47
  result = {}
47
- # matches: export KEY=value or KEY=value (with optional ecport and quotes)
48
+ # matches: export KEY=value or KEY=value
49
+ # This regex can clean up and capture lines whether they are written as export KEY="value",
50
+ # or as KEY='value' or simply KEY=value.
48
51
  pattern = re.compile(r"""^\s*(?:export\s+)?(\w+)=["\']?([^"\';\n]*)["\']?\s*$""")
49
52
 
50
53
  for line in path.read_text().splitlines():
@@ -58,14 +61,11 @@ def _parse_rc_file(path: Path) -> dict:
58
61
  result[field] = value
59
62
  return result
60
63
 
61
- #clouds.yaml parser
62
-
63
64
  def _parse_clouds_yaml(path: Path, cloud_name: Optional[str] = None) -> dict:
64
65
  """
65
66
  Parse a clouds.yaml file.
66
67
  If cloud_name is None and there is only one cloud entry, use that one.
67
- """
68
- # NOTE: put it in the requirement
68
+ """
69
69
  if not HAS_YAML:
70
70
  raise ImportError("PyYAML is required to parse clouds.yaml. Run: pip install pyyaml")
71
71
 
@@ -76,8 +76,7 @@ def _parse_clouds_yaml(path: Path, cloud_name: Optional[str] = None) -> dict:
76
76
  raise ValueError("No 'clouds' section found in the file.")
77
77
 
78
78
  # auto-select if only one cloud
79
- # if user hasn't specifyed with parser
80
- # NOTE: to be removed outside test
79
+ # if user hasn't specifyed with parser
81
80
  if cloud_name is None:
82
81
  if len(clouds) == 1:
83
82
  cloud_name = next(iter(clouds))
@@ -96,7 +95,7 @@ def _parse_clouds_yaml(path: Path, cloud_name: Optional[str] = None) -> dict:
96
95
  result = {}
97
96
 
98
97
  # auth block
99
- # add fields to the result
98
+ # NOTE: add fields to the result
100
99
  if auth.get("auth_url"):
101
100
  result["openstack_auth_url"] = auth["auth_url"]
102
101
  if auth.get("application_credential_id"):
@@ -109,11 +108,8 @@ def _parse_clouds_yaml(path: Path, cloud_name: Optional[str] = None) -> dict:
109
108
  result["openstack_interface"] = entry["interface"]
110
109
  if entry.get("identity_api_version"):
111
110
  result["openstack_identity_api_version"] = str(entry["identity_api_version"])
112
-
113
111
  return result
114
112
 
115
- # Public API
116
-
117
113
  def parse_credential_file(path: str, cloud_name: Optional[str] = None) -> dict:
118
114
  """
119
115
  Auto-detect file type (.sh / .yaml / .yml) and return a dict of credentials.
@@ -137,7 +133,7 @@ def parse_credential_file(path: str, cloud_name: Optional[str] = None) -> dict:
137
133
  elif suffix in (".sh", ".env", ""):
138
134
  creds = _parse_rc_file(p)
139
135
  else:
140
- # try RC first, fall back to YAML
136
+ # ambiguous suffix: try RC first, fall back to YAML
141
137
  try:
142
138
  creds = _parse_rc_file(p)
143
139
  if not creds:
@@ -1,6 +1,8 @@
1
1
  """
2
2
  PostgreSQL connection.
3
- The agent has NO direct DB access, **all writes go through the API**
3
+ The agent has NO direct DB access:
4
+ **all writes go through the API**
5
+ The Dashboard is stateless.
4
6
  """
5
7
 
6
8
  import os
@@ -35,6 +37,8 @@ def create_deployment(
35
37
  conn = get_conn()
36
38
  try:
37
39
  with conn.cursor() as cur:
40
+ # If the user making the request doesn't yet exist in the local database, it inserts them.
41
+ #if they already exist (subprimary key conflict), it does nothing.
38
42
  cur.execute(
39
43
  """
40
44
  INSERT INTO users (sub, username, email, role, active)
@@ -43,6 +47,8 @@ def create_deployment(
43
47
  """,
44
48
  (user_sub, username, ""),
45
49
  )
50
+ #Create a row in the deployments table with the initial state set to "QUEUED."
51
+ # if, by chance, that UUID already exists, simply update the state and date.
46
52
  cur.execute(
47
53
  """
48
54
  INSERT INTO deployments (
@@ -69,6 +75,8 @@ def update_status(
69
75
  conn = get_conn()
70
76
  try:
71
77
  with conn.cursor() as cur:
78
+ # COALESCE causes the database to keep old value already present
79
+ # without overwriting them with an empty value.
72
80
  cur.execute(
73
81
  """
74
82
  UPDATE deployments
@@ -16,6 +16,11 @@ import sys
16
16
 
17
17
 
18
18
  def _add_to_path():
19
+ """
20
+ This function opens your Bash shell configuration file (~/.bashrc).
21
+ It checks whether the ~/.local/bin folder is already present. If it isn't,
22
+ it adds the following line to the bottom of the file: export PATH=$HOME/.local/bin:$PATH
23
+ """
19
24
  bashrc = os.path.expanduser("~/.bashrc")
20
25
  line = 'export PATH=$HOME/.local/bin:$PATH'
21
26
 
@@ -33,26 +38,34 @@ def _add_to_path():
33
38
 
34
39
 
35
40
  def _create_workdir(workdir: str):
41
+ """
42
+ Create the main folder where the application will run (by default, ~/laniakea-api)
43
+ and create a certs subfolder within it to host future (optional) SSL certificates.
44
+
45
+ The most important part is generating the .env file. If the file doesn't already exist, it will
46
+ be created using the default values managed in the CONFIG.py
47
+ """
36
48
  os.makedirs(workdir, exist_ok=True)
37
49
  os.makedirs(os.path.join(workdir, "certs"), exist_ok=True)
38
50
 
39
51
  env_path = os.path.join(workdir, ".env")
40
52
  if os.path.exists(env_path):
41
- print(f"[workdir] .env already exists skip")
53
+ print(f"[workdir] .env already exists... skipping the creation")
42
54
  else:
55
+ # NOTE: MOD. HERE THE TEMPLATE
43
56
  template = """\
44
57
  # ── Auth ──────────────────────────────────────────────────────────────────────
45
58
  # Generate with: python3 -c "import secrets; print(secrets.token_hex(32))"
46
59
  SECRET_KEY=
47
60
  SESSION_TTL_MINUTES=60
48
- OIDC_DISCOVERY_URL=https://iam.recas.ba.infn.it/.well-known/openid-configuration
61
+ OIDC_DISCOVERY_URL=https://example.it/.well-known/openid-configuration
49
62
 
50
63
  # ── Agent pool password (must match the agent .env) ───────────────────────────
51
64
  AGENT_MASTER_PASSWORD=
52
65
 
53
66
  # ── Redis ─────────────────────────────────────────────────────────────────────
54
67
  REDIS_HOST=127.0.0.1
55
- REDIS_PORT=1908
68
+ REDIS_PORT=
56
69
  REDIS_PASSWORD=
57
70
 
58
71
  # ── PostgreSQL ────────────────────────────────────────────────────────────────
@@ -83,20 +96,28 @@ DEPLOYMENT_LOG_DIR=/var/log/laniakea-agent
83
96
 
84
97
 
85
98
  def _create_log_dir():
99
+ """
100
+ The API and agents need to write deployment logs to /var/log/laniakea-agent.
101
+ The script create this system folder if needed.
102
+ """
86
103
  log_dir = "/var/log/laniakea-agent"
87
104
  if os.path.exists(log_dir):
88
- print(f"[logs] {log_dir} already exists skip")
105
+ print(f"[logs] {log_dir} already exists... skipping the creation")
89
106
  return
90
107
  try:
91
108
  os.makedirs(log_dir, exist_ok=True)
92
109
  os.chown(log_dir, os.getuid(), os.getgid())
93
110
  print(f"[logs] log directory created: {log_dir}")
94
111
  except PermissionError:
95
- print(f"[logs] insufficient permissions please run:")
112
+ print(f"[logs] insufficient permissions! Please run:")
96
113
  print(f" sudo mkdir -p {log_dir} && sudo chown $USER:$USER {log_dir}")
97
114
 
98
115
 
99
116
  def main():
117
+ """
118
+ Once the installation is completed this function print
119
+ a guide for the user.
120
+ """
100
121
  parser = argparse.ArgumentParser(
101
122
  prog="laniakea-api-install",
102
123
  description="Initial setup of the VM for laniakea-api-server.",
@@ -114,6 +135,7 @@ def main():
114
135
  _create_log_dir()
115
136
  _create_workdir(args.workdir)
116
137
 
138
+ # FIXME: implement a guide for certifacete (non-self-signed)
117
139
  print(f"""
118
140
  === Setup completed ===
119
141
 
@@ -1,5 +1,8 @@
1
1
  """
2
- Registers all routers and starts uvicorn
2
+ Registers all routers and starts uvicorn.
3
+
4
+ Create the FastAPI application instance and map the various groups of
5
+ endpoints (the "routers") to their respective URL addresses (the "prefixes").
3
6
  """
4
7
 
5
8
  import os
@@ -8,11 +11,11 @@ from fastapi import FastAPI
8
11
  from laniakea_api.routers import agent, agents,credentials, deployments, health
9
12
 
10
13
  # FastAPI App
11
-
12
14
  app = FastAPI(
13
15
  title="Laniakea Queue API",
14
16
  description="OIDC-authenticated gateway for enqueuing cloud deployment jobs.",
15
- version="1.4.0",
17
+ # FIXME: automatizza versione
18
+ version="0.1.4",
16
19
  )
17
20
 
18
21
  # NOTE: CHANGE HERE for path
@@ -24,8 +27,8 @@ app.include_router(credentials.router, prefix=BASE)
24
27
  app.include_router(deployments.router, prefix=BASE)
25
28
  app.include_router(agent.router, prefix=INTERNAL)
26
29
  app.include_router(agents.router, prefix=INTERNAL) # POST /internal/agents/heartbeat
27
- app.include_router(agents.router, prefix=BASE) # GET /api/agents/status
28
- app.include_router(health.router) # /health:no prefix
30
+ app.include_router(agents.router, prefix=BASE) # GET /api/agents/status
31
+ app.include_router(health.router) # /health:no prefix
29
32
 
30
33
  ############ Routes registered ############################
31
34
  #NOTE: Add or remove every new modification
@@ -47,6 +50,7 @@ app.include_router(health.router) # /health:no prefix
47
50
  # GET /health
48
51
 
49
52
  # Entry point uvicorn
53
+ # skipped
50
54
  if __name__ == "__main__":
51
55
  uvicorn.run(
52
56
  "main:app",
@@ -1,5 +1,7 @@
1
1
  """
2
- all Pydantic models used across the api.
2
+ All Pydantic models used across the api.
3
+
4
+ Defines the data structure of everything that enters/exits from the API
3
5
  """
4
6
 
5
7
  from typing import Optional
@@ -9,13 +11,14 @@ from pydantic import BaseModel
9
11
  class OIDCLoginRequest(BaseModel):
10
12
  """
11
13
  Only the aai token is needed.
14
+ From idp.
12
15
  """
13
16
  oidc_token: str
14
17
 
15
18
 
16
19
  class SessionTokenResponse(BaseModel):
17
20
  """
18
- If the token check is OK returns:
21
+ If the token check is OK returns a JWT containing:
19
22
  """
20
23
  session_token: str
21
24
  token_type: str = "bearer" # checks if the user has the correct permission
@@ -55,7 +58,7 @@ class DeploymentRequest(BaseModel):
55
58
  """
56
59
  deployment_uuid: str # NOTE: the dashboard needs to create a uuid for each job
57
60
  timestamp: str
58
- description: str # optional or mandatory? check teams
61
+ description: str # NOTE: optional or mandatory? check teams
59
62
  auth: dict # { aai_token, sub, group }
60
63
  orchestrator: dict # target_provider, desired_orchestrator, endpoint
61
64
  selected_provider: str # OpenStack | AWS
@@ -21,7 +21,7 @@ def _validate_transition(current: str, new_status: str, uuid: str) -> None:
21
21
  """
22
22
  allowed = {
23
23
  "QUEUED": {"CREATE_IN_PROGRESS", "UPDATE_IN_PROGRESS"},
24
- "CREATE_IN_PROGRESS": {"CREATE_COMPLETE", "CREATE_FAILED"},
24
+ "CREATE_IN_PROGRESS": {"CREATE_COMPLETE", "CREATE_FAILED", "QUEUED"},
25
25
  "UPDATE_IN_PROGRESS": {"UPDATE_FAILED"},
26
26
  "CREATE_COMPLETE": set(),
27
27
  "CREATE_FAILED": set(),
@@ -26,6 +26,7 @@ class QuotaInfo(BaseModel):
26
26
  floating_ips_available: Optional[int] = None
27
27
 
28
28
 
29
+ # NOTE: is it ok every 30 sec?
29
30
  class HeartbeatRequest(BaseModel):
30
31
  """
31
32
  Sent by the agent every 30 seconds
@@ -53,7 +54,7 @@ async def agent_heartbeat(
53
54
  ):
54
55
  """
55
56
  Called by laniakea-agent every 30 seconds.
56
- Stores the agent's quota info in Redis with a 60-second TTL.
57
+ Stores the agent's quota info in Redis with a 60 second TTL.
57
58
  If the agent stops sending heartbeats, the key expires and the
58
59
  dashboard shows the agent as offline.
59
60
  """
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "laniakea-api-server"
7
- version = "0.1.2"
7
+ version = "0.2.0"
8
8
  description = "Laniakea Queue API — OIDC-authenticated gateway for cloud deployment orchestration"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.10"
12
12
 
13
13
  authors = [
14
- { name = "Laniakea Team" }
14
+ { name = "Riccardo Caccia" }
15
15
  ]
16
16
 
17
17
  keywords = ["cloud", "deployment", "openstack", "aws", "fastapi", "redis", "orchestration"]
@@ -1,103 +0,0 @@
1
- """
2
- Authentication helpers.
3
- """
4
- import time
5
- import httpx
6
- import jwt
7
- from fastapi import Depends, HTTPException, status
8
- from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
9
- from laniakea_api.config import (
10
- SECRET_KEY, ALGORITHM, SESSION_TTL_MINUTES,
11
- OIDC_DISCOVERY_URL, AGENT_MASTER_PASSWORD,
12
- )
13
-
14
- _bearer = HTTPBearer()
15
-
16
-
17
- async def fetch_userinfo(oidc_token: str) -> dict:
18
- try:
19
- async with httpx.AsyncClient(timeout=10) as client:
20
- discovery = await client.get(OIDC_DISCOVERY_URL)
21
- discovery.raise_for_status()
22
- userinfo_url = discovery.json()["userinfo_endpoint"]
23
- resp = await client.get(
24
- userinfo_url,
25
- headers={"Authorization": f"Bearer {oidc_token}"},
26
- )
27
- if resp.status_code != 200:
28
- raise HTTPException(
29
- status_code=status.HTTP_401_UNAUTHORIZED,
30
- detail=f"OIDC userinfo returned {resp.status_code}",
31
- )
32
- return resp.json()
33
- except HTTPException:
34
- raise
35
- except Exception as exc:
36
- raise HTTPException(
37
- status_code=status.HTTP_401_UNAUTHORIZED,
38
- detail=f"OIDC validation failed: {exc}",
39
- )
40
-
41
-
42
- def create_session_token(user_info: dict) -> tuple:
43
- expires_in = SESSION_TTL_MINUTES * 60
44
- now = int(time.time())
45
- payload = {
46
- "sub": user_info.get("sub"),
47
- "username": user_info.get("preferred_username") or user_info.get("sub"),
48
- "email": user_info.get("email"),
49
- "groups": user_info.get("groups", []),
50
- "iat": now,
51
- "exp": now + expires_in,
52
- }
53
- return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM), expires_in
54
-
55
-
56
- async def verify_session_token(
57
- credentials: HTTPAuthorizationCredentials = Depends(_bearer),
58
- ) -> dict:
59
- try:
60
- return jwt.decode(
61
- credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM]
62
- )
63
- except jwt.ExpiredSignatureError:
64
- raise HTTPException(
65
- status_code=status.HTTP_401_UNAUTHORIZED,
66
- detail="Session token expired.",
67
- )
68
- except jwt.InvalidTokenError as exc:
69
- raise HTTPException(
70
- status_code=status.HTTP_401_UNAUTHORIZED,
71
- detail=f"Invalid session token: {exc}",
72
- )
73
-
74
-
75
- async def verify_agent_token(
76
- credentials: HTTPAuthorizationCredentials = Depends(_bearer),
77
- ) -> str:
78
- """
79
- Validates the agent JWT signed with AGENT_MASTER_PASSWORD.
80
- HTCondor pool-password model — one shared secret for all agents.
81
- """
82
- if not AGENT_MASTER_PASSWORD:
83
- raise HTTPException(
84
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
85
- detail="AGENT_MASTER_PASSWORD not configured.",
86
- )
87
- try:
88
- payload = jwt.decode(
89
- credentials.credentials,
90
- AGENT_MASTER_PASSWORD,
91
- algorithms=["HS256"],
92
- )
93
- return payload.get("sub", "unknown-agent")
94
- except jwt.ExpiredSignatureError:
95
- raise HTTPException(
96
- status_code=status.HTTP_401_UNAUTHORIZED,
97
- detail="Agent token expired.",
98
- )
99
- except jwt.InvalidTokenError as exc:
100
- raise HTTPException(
101
- status_code=status.HTTP_401_UNAUTHORIZED,
102
- detail=f"Invalid agent token: {exc}",
103
- )