vantage-cli 0.1.1__py3-none-any.whl

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 (46) hide show
  1. vantage_cli/__init__.py +131 -0
  2. vantage_cli/apps/__init__.py +22 -0
  3. vantage_cli/apps/common.py +78 -0
  4. vantage_cli/apps/juju_localhost/__init__.py +17 -0
  5. vantage_cli/apps/juju_localhost/app.py +255 -0
  6. vantage_cli/apps/juju_localhost/bundle_yaml.py +143 -0
  7. vantage_cli/apps/microk8s/README.md +47 -0
  8. vantage_cli/apps/microk8s/__init__.py +3 -0
  9. vantage_cli/apps/microk8s/app.py +301 -0
  10. vantage_cli/apps/multipass_singlenode/__init__.py +12 -0
  11. vantage_cli/apps/multipass_singlenode/app.py +173 -0
  12. vantage_cli/apps/templates.py +178 -0
  13. vantage_cli/auth.py +429 -0
  14. vantage_cli/cache.py +143 -0
  15. vantage_cli/client.py +84 -0
  16. vantage_cli/command_base.py +63 -0
  17. vantage_cli/commands/__init__.py +1 -0
  18. vantage_cli/commands/clouds/__init__.py +20 -0
  19. vantage_cli/commands/clouds/add.py +81 -0
  20. vantage_cli/commands/clouds/delete.py +61 -0
  21. vantage_cli/commands/clouds/render.py +146 -0
  22. vantage_cli/commands/clouds/update.py +97 -0
  23. vantage_cli/commands/clusters/__init__.py +27 -0
  24. vantage_cli/commands/clusters/create.py +270 -0
  25. vantage_cli/commands/clusters/delete.py +101 -0
  26. vantage_cli/commands/clusters/get.py +30 -0
  27. vantage_cli/commands/clusters/list.py +84 -0
  28. vantage_cli/commands/clusters/render.py +233 -0
  29. vantage_cli/commands/clusters/schema.py +31 -0
  30. vantage_cli/commands/clusters/utils.py +248 -0
  31. vantage_cli/commands/profile/__init__.py +30 -0
  32. vantage_cli/commands/profile/crud.py +529 -0
  33. vantage_cli/commands/profile/render.py +55 -0
  34. vantage_cli/config.py +161 -0
  35. vantage_cli/constants.py +40 -0
  36. vantage_cli/exceptions.py +127 -0
  37. vantage_cli/format.py +39 -0
  38. vantage_cli/gql_client.py +655 -0
  39. vantage_cli/main.py +303 -0
  40. vantage_cli/render.py +56 -0
  41. vantage_cli/schemas.py +48 -0
  42. vantage_cli/time_loop.py +124 -0
  43. vantage_cli-0.1.1.dist-info/METADATA +30 -0
  44. vantage_cli-0.1.1.dist-info/RECORD +46 -0
  45. vantage_cli-0.1.1.dist-info/WHEEL +4 -0
  46. vantage_cli-0.1.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,131 @@
1
+ """Vantage CLI package for managing cloud computing resources."""
2
+
3
+ import asyncio
4
+ import importlib.metadata
5
+ import inspect
6
+ from typing import Any, Callable, Optional
7
+
8
+ import typer
9
+
10
+ __version__ = importlib.metadata.version("vantage-cli")
11
+
12
+
13
+ class AsyncTyper(typer.Typer):
14
+ """A Typer subclass that automatically wraps async functions with asyncio.run()."""
15
+
16
+ @staticmethod
17
+ def maybe_run_async(func: Callable, *args: Any, **kwargs: Any) -> Any:
18
+ """Run function asynchronously if it's a coroutine, otherwise run normally."""
19
+ if inspect.iscoroutinefunction(func):
20
+ return asyncio.run(func(*args, **kwargs))
21
+ return func(*args, **kwargs)
22
+
23
+ def command(
24
+ self,
25
+ name: Optional[str] = None,
26
+ *,
27
+ cls: Optional[type] = None,
28
+ context_settings: Optional[dict] = None,
29
+ help: Optional[str] = None,
30
+ epilog: Optional[str] = None,
31
+ short_help: Optional[str] = None,
32
+ options_metavar: Optional[str] = None,
33
+ add_help_option: bool = True,
34
+ no_args_is_help: bool = False,
35
+ hidden: bool = False,
36
+ deprecated: bool = False,
37
+ rich_help_panel: Optional[str] = None,
38
+ ):
39
+ """Override command decorator to handle async functions."""
40
+
41
+ def decorator(func: Callable) -> Callable:
42
+ if inspect.iscoroutinefunction(func):
43
+ # Create a sync wrapper that preserves the original function signature
44
+ import functools
45
+
46
+ @functools.wraps(func)
47
+ def sync_wrapper(*args, **kwargs):
48
+ return self.maybe_run_async(func, *args, **kwargs)
49
+
50
+ wrapped_func = sync_wrapper
51
+ else:
52
+ wrapped_func = func
53
+
54
+ # Build kwargs for parent method, filtering out None values
55
+ kwargs = {
56
+ "name": name,
57
+ "cls": cls,
58
+ "context_settings": context_settings,
59
+ "help": help,
60
+ "epilog": epilog,
61
+ "short_help": short_help,
62
+ "add_help_option": add_help_option,
63
+ "no_args_is_help": no_args_is_help,
64
+ "hidden": hidden,
65
+ "deprecated": deprecated,
66
+ "rich_help_panel": rich_help_panel,
67
+ }
68
+ if options_metavar is not None:
69
+ kwargs["options_metavar"] = options_metavar
70
+
71
+ return super(AsyncTyper, self).command(**kwargs)(wrapped_func)
72
+
73
+ return decorator
74
+
75
+ def callback(
76
+ self,
77
+ *,
78
+ cls: Optional[type] = None,
79
+ invoke_without_command: bool = False,
80
+ no_args_is_help: bool = False,
81
+ subcommand_metavar: Optional[str] = None,
82
+ chain: bool = False,
83
+ result_callback: Optional[Callable] = None,
84
+ context_settings: Optional[dict] = None,
85
+ help: Optional[str] = None,
86
+ epilog: Optional[str] = None,
87
+ short_help: Optional[str] = None,
88
+ options_metavar: Optional[str] = None,
89
+ add_help_option: bool = True,
90
+ hidden: bool = False,
91
+ deprecated: bool = False,
92
+ rich_help_panel: Optional[str] = None,
93
+ ):
94
+ """Override callback decorator to handle async functions."""
95
+
96
+ def decorator(func: Callable) -> Callable:
97
+ if inspect.iscoroutinefunction(func):
98
+ # Create a sync wrapper that preserves the original function signature
99
+ import functools
100
+
101
+ @functools.wraps(func)
102
+ def sync_wrapper(*args, **kwargs):
103
+ return self.maybe_run_async(func, *args, **kwargs)
104
+
105
+ wrapped_func = sync_wrapper
106
+ else:
107
+ wrapped_func = func
108
+
109
+ # Build kwargs for parent method, filtering out None values
110
+ kwargs = {
111
+ "cls": cls,
112
+ "invoke_without_command": invoke_without_command,
113
+ "no_args_is_help": no_args_is_help,
114
+ "subcommand_metavar": subcommand_metavar,
115
+ "chain": chain,
116
+ "result_callback": result_callback,
117
+ "context_settings": context_settings,
118
+ "help": help,
119
+ "epilog": epilog,
120
+ "short_help": short_help,
121
+ "add_help_option": add_help_option,
122
+ "hidden": hidden,
123
+ "deprecated": deprecated,
124
+ "rich_help_panel": rich_help_panel,
125
+ }
126
+ if options_metavar is not None:
127
+ kwargs["options_metavar"] = options_metavar
128
+
129
+ return super(AsyncTyper, self).callback(**kwargs)(wrapped_func)
130
+
131
+ return decorator
@@ -0,0 +1,22 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """Cluster management commands for Vantage CLI."""
4
+
5
+ from vantage_cli import AsyncTyper
6
+ from vantage_cli.apps.juju_localhost import juju_localhost_app
7
+ from vantage_cli.apps.multipass_singlenode import multipass_singlenode_app
8
+
9
+ # Create the apps command group
10
+ apps_app = AsyncTyper(
11
+ name="apps",
12
+ help="Vantage infrastructure automation applications.",
13
+ context_settings={
14
+ "allow_extra_args": True,
15
+ "allow_interspersed_args": True,
16
+ "ignore_unknown_options": True,
17
+ },
18
+ )
19
+
20
+ # Register subcommand groups
21
+ apps_app.add_typer(juju_localhost_app)
22
+ apps_app.add_typer(multipass_singlenode_app)
@@ -0,0 +1,78 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """Common validation utilities for deployment apps."""
4
+
5
+ from typing import Any, Dict, Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from vantage_cli.constants import (
11
+ ERROR_NO_CLIENT_ID,
12
+ ERROR_NO_CLIENT_SECRET,
13
+ ERROR_NO_CLUSTER_DATA,
14
+ )
15
+
16
+
17
+ def validate_cluster_data(
18
+ cluster_data: Optional[Dict[str, Any]], console: Console
19
+ ) -> Dict[str, Any]:
20
+ """Validate that cluster data exists and contains required fields.
21
+
22
+ Args:
23
+ cluster_data: Optional cluster configuration dictionary
24
+ console: Rich console for error output
25
+
26
+ Returns:
27
+ Validated cluster data dictionary
28
+
29
+ Raises:
30
+ typer.Exit: If validation fails
31
+ """
32
+ if not cluster_data:
33
+ console.print(ERROR_NO_CLUSTER_DATA)
34
+ raise typer.Exit(code=1)
35
+ return cluster_data
36
+
37
+
38
+ def validate_client_credentials(
39
+ cluster_data: Dict[str, Any], console: Console
40
+ ) -> tuple[str, Optional[str]]:
41
+ """Validate and extract client credentials from cluster data.
42
+
43
+ Args:
44
+ cluster_data: Cluster configuration dictionary
45
+ console: Rich console for error output
46
+
47
+ Returns:
48
+ Tuple of (client_id, client_secret) where client_secret may be None
49
+
50
+ Raises:
51
+ typer.Exit: If client_id is missing
52
+ """
53
+ client_id = cluster_data.get("clientId", None)
54
+ if not client_id:
55
+ console.print(ERROR_NO_CLIENT_ID)
56
+ raise typer.Exit(code=1)
57
+
58
+ client_secret = cluster_data.get("clientSecret", None)
59
+ return client_id, client_secret
60
+
61
+
62
+ def require_client_secret(client_secret: Optional[str], console: Console) -> str:
63
+ """Validate that client secret exists.
64
+
65
+ Args:
66
+ client_secret: Optional client secret string
67
+ console: Rich console for error output
68
+
69
+ Returns:
70
+ Validated client secret string
71
+
72
+ Raises:
73
+ typer.Exit: If client secret is missing
74
+ """
75
+ if not client_secret:
76
+ console.print(ERROR_NO_CLIENT_SECRET)
77
+ raise typer.Exit(code=1)
78
+ return client_secret
@@ -0,0 +1,17 @@
1
+ """Juju localhost deployment app package."""
2
+
3
+ from vantage_cli import AsyncTyper
4
+
5
+ from .app import deploy_command
6
+
7
+ juju_localhost_app = AsyncTyper(
8
+ name="juju-localhost",
9
+ help="Juju localhost SLURM application commands.",
10
+ context_settings={
11
+ "allow_extra_args": True,
12
+ "allow_interspersed_args": True,
13
+ "ignore_unknown_options": True,
14
+ },
15
+ )
16
+
17
+ juju_localhost_app.command("deploy")(deploy_command)
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2025 Vantage Compute Corporation
3
+ # See LICENSE file for licensing details.
4
+ """Juju localhost deployment app for Vantage CLI."""
5
+
6
+ import os
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Any, Dict, Optional
10
+
11
+ import typer
12
+ import yaml
13
+ from juju.controller import Controller
14
+ from juju.errors import JujuError
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+ from typing_extensions import Annotated
18
+
19
+ from vantage_cli.apps.common import validate_client_credentials, validate_cluster_data
20
+ from vantage_cli.config import attach_settings
21
+ from vantage_cli.constants import (
22
+ ENV_CLIENT_SECRET,
23
+ JUJU_APPLICATION_NAME,
24
+ JUJU_SECRET_NAME,
25
+ )
26
+
27
+ from .bundle_yaml import VANTAGE_JUPYTERHUB_YAML
28
+
29
+
30
+ def _build_secret_args(ctx: Any) -> list[str]:
31
+ return [
32
+ f"oidc-client-id={ctx.client_id}",
33
+ f"oidc-client-secret={ctx.client_secret}",
34
+ f"oidc-base-url={ctx.oidc_base_url}",
35
+ f"tunnel-api-url={ctx.tunnel_api_url}",
36
+ f"vantage-api-url={ctx.base_api_url}",
37
+ f"oidc-domain={ctx.oidc_domain}",
38
+ f"jupyterhub-token={ctx.jupyterhub_token}",
39
+ ]
40
+
41
+
42
+ def _prepare_bundle(ctx: Any, model_name: str, secret_id: str) -> dict[str, Any]:
43
+ bundle_yaml = VANTAGE_JUPYTERHUB_YAML.copy()
44
+ bundle_yaml["applications"]["slurmctld"]["options"]["cluster-name"] = model_name
45
+ va_opts = bundle_yaml["applications"]["vantage-agent"]["options"]
46
+ jb_opts = bundle_yaml["applications"]["jobbergate-agent"]["options"]
47
+ hub_opts = bundle_yaml["applications"]["vantage-jupyterhub"]["options"]
48
+
49
+ va_opts["vantage-agent-base-api-url"] = ctx.base_api_url
50
+ va_opts["vantage-agent-oidc-client-id"] = ctx.client_id
51
+ va_opts["vantage-agent-oidc-domain"] = ctx.oidc_domain
52
+ va_opts["vantage-agent-oidc-client-secret"] = ctx.client_secret
53
+ va_opts["vantage-agent-cluster-name"] = model_name
54
+
55
+ jb_opts["jobbergate-agent-base-api-url"] = ctx.base_api_url
56
+ jb_opts["jobbergate-agent-oidc-domain"] = ctx.oidc_domain
57
+ jb_opts["jobbergate-agent-oidc-client-id"] = ctx.client_id
58
+ jb_opts["jobbergate-agent-oidc-client-secret"] = ctx.client_secret
59
+
60
+ hub_opts["vantage-jupyterhub-config-secret-id"] = secret_id
61
+ return bundle_yaml
62
+
63
+
64
+ async def _write_and_deploy_model_bundle(model, bundle_yaml: dict[str, Any]) -> None:
65
+ original_cwd = os.getcwd()
66
+ with tempfile.TemporaryDirectory() as td:
67
+ f_name = Path(td) / "bundle.yaml"
68
+ with open(f_name, "w") as fh:
69
+ fh.write(yaml.dump(bundle_yaml))
70
+ Path(td).chmod(0o700)
71
+ os.chdir(td)
72
+ try:
73
+ await model.deploy("./bundle.yaml")
74
+ finally:
75
+ os.chdir(original_cwd)
76
+
77
+
78
+ async def _run_slurmd_node_configured(model) -> None:
79
+ if slurmd_app := model.applications.get("slurmd"):
80
+ if slurmd_units := slurmd_app.units:
81
+ for slurmd_unit in slurmd_units:
82
+ action = await slurmd_unit.run_action("node-configured")
83
+ await action.wait()
84
+ print(f"Action result for {slurmd_unit.name}: {action.results}")
85
+
86
+
87
+ async def _configure_jobbergate_influxdb(model) -> None:
88
+ slurmctld_app = model.applications.get("slurmctld")
89
+ if not slurmctld_app or not slurmctld_app.units:
90
+ print("Warning: slurmctld application not found")
91
+ return
92
+ leader_unit = None
93
+ for unit in slurmctld_app.units:
94
+ if await unit.is_leader_from_status():
95
+ leader_unit = unit
96
+ break
97
+ if leader_unit is None:
98
+ print("Warning: Could not find slurmctld leader unit")
99
+ return
100
+ action = await leader_unit.run("sudo cat /etc/slurm/acct_gather.conf")
101
+ await action.wait()
102
+ if action.results.get("return-code") != 0:
103
+ print(
104
+ f"Warning: Failed to get InfluxDB config: {action.results.get('stderr', 'Unknown error')}"
105
+ )
106
+ return
107
+ influxdb_conf = action.results.get("stdout", "")
108
+ host = user = pw = db = rp = None
109
+ for line in influxdb_conf.splitlines():
110
+ if line.startswith("profileinfluxdbhost"):
111
+ host = line.split("=", 1)[1].strip()
112
+ elif line.startswith("profileinfluxdbuser"):
113
+ user = line.split("=", 1)[1].strip()
114
+ elif line.startswith("profileinfluxdbpass"):
115
+ pw = line.split("=", 1)[1].strip()
116
+ elif line.startswith("profileinfluxdbdatabase"):
117
+ db = line.split("=", 1)[1].strip()
118
+ elif line.startswith("profileinfluxdbrtpolicy"):
119
+ rp = line.split("=", 1)[1].strip()
120
+ print(f"Parsed InfluxDB config: host={host}, user={user}, db={db}, rp={rp}")
121
+ if not all([user, pw, host, db, rp]):
122
+ print("Warning: Could not parse complete InfluxDB configuration")
123
+ return
124
+ influxdb_uri = f"influxdb://{user}:{pw}@{host}/{db}?rp={rp}"
125
+ jobbergate_agent = model.applications.get("jobbergate-agent")
126
+ if not jobbergate_agent:
127
+ print("Warning: jobbergate-agent application not found")
128
+ return
129
+ await jobbergate_agent.set_config({"jobbergate-agent-influx-dsn": influxdb_uri})
130
+ print("InfluxDB configuration applied to jobbergate-agent")
131
+
132
+
133
+ async def deploy_juju_localhost(ctx: Any) -> None | typer.Exit:
134
+ """Deploy Vantage JupyterHub SLURM cluster using Juju on localhost (refactored)."""
135
+ console = Console()
136
+ controller = Controller()
137
+ model_name = "-".join(ctx.client_id.split("-")[:-4])
138
+ secret_name = JUJU_SECRET_NAME
139
+ try:
140
+ console.print("Connecting to Juju controller...")
141
+ await controller.connect()
142
+ console.print(f"Creating juju model: {model_name}")
143
+ model = await controller.add_model(model_name, cloud_name="localhost")
144
+ try:
145
+ console.print(f"Creating '{secret_name}' juju secret...")
146
+ secret = await model.add_secret(secret_name, _build_secret_args(ctx))
147
+ console.print(f"Secret created with ID: {secret}")
148
+ console.print("Deploying SLURM cluster...")
149
+ bundle_yaml = _prepare_bundle(ctx, model_name, secret)
150
+ await _write_and_deploy_model_bundle(model, bundle_yaml)
151
+ await model.grant_secret(secret_name, JUJU_APPLICATION_NAME)
152
+ try:
153
+ await model.wait_for_idle()
154
+ except JujuError:
155
+ return typer.Exit(code=1)
156
+ await _run_slurmd_node_configured(model)
157
+ await _configure_jobbergate_influxdb(model)
158
+ print(
159
+ f"'{model_name}' deployment complete!\nModel: {model_name}\nController: {controller.controller_name}"
160
+ )
161
+ finally:
162
+ await model.disconnect()
163
+ finally:
164
+ await controller.disconnect()
165
+
166
+
167
+ async def deploy(ctx: typer.Context, cluster_data: Optional[Dict[str, Any]] = None) -> None:
168
+ """Deploy Juju localhost SLURM cluster using cluster data.
169
+
170
+ Args:
171
+ ctx: Typer context containing CLI configuration
172
+ cluster_data: Optional cluster configuration dictionary with client credentials
173
+
174
+ Raises:
175
+ typer.Exit: If deployment fails due to missing or invalid cluster data
176
+ """
177
+ console = Console()
178
+ console.print(Panel("Juju Localhost SLURM Application"))
179
+ console.print("Deploying juju localhost slurm application...")
180
+
181
+ # Validate cluster data and extract credentials
182
+ cluster_data = validate_cluster_data(cluster_data, console)
183
+ client_id, client_secret = validate_client_credentials(cluster_data, console)
184
+
185
+ # Get client secret from API if not in cluster data (import locally to avoid circular import)
186
+ if not client_secret:
187
+ from vantage_cli.commands.clusters import utils as cluster_utils
188
+
189
+ client_secret = await cluster_utils.get_cluster_client_secret(ctx=ctx, client_id=client_id)
190
+
191
+ # Check environment variable as fallback for client secret
192
+ if not client_secret:
193
+ client_secret = os.environ.get(ENV_CLIENT_SECRET, None)
194
+ if not client_secret:
195
+ console.print(
196
+ "[red]Error: No client secret found in cluster data, API, or environment.[/red]"
197
+ )
198
+ raise typer.Exit(code=1)
199
+
200
+ # Get jupyterhub_token from cluster data if available, otherwise generate a default
201
+ jupyterhub_token = None
202
+ if cluster_data and "creationParameters" in cluster_data:
203
+ if jupyterhub_token_data := cluster_data["creationParameters"].get("jupyterhub_token"):
204
+ jupyterhub_token = jupyterhub_token_data
205
+
206
+ if jupyterhub_token is None:
207
+ console.print("[red]Error: No jupyterhub_token found in cluster data.[/red]")
208
+ raise typer.Exit(code=1)
209
+
210
+ # Temporarily disabled for testing
211
+ # vantage_cluster_context = VantageClusterContext(
212
+ class MockContext:
213
+ def __init__(self):
214
+ self.client_id = client_id
215
+ self.client_secret = client_secret
216
+ self.base_api_url = "http://localhost:8000" # dummy
217
+ self.oidc_base_url = "http://localhost:8001" # dummy
218
+ self.oidc_domain = "localhost" # dummy
219
+ self.tunnel_api_url = "http://localhost:8002" # dummy
220
+ self.jupyterhub_token = jupyterhub_token
221
+
222
+ vantage_cluster_context = MockContext()
223
+ await deploy_juju_localhost(vantage_cluster_context)
224
+
225
+
226
+ # Typer CLI commands
227
+ @attach_settings
228
+ async def deploy_command(
229
+ ctx: typer.Context,
230
+ cluster_name: Annotated[
231
+ str,
232
+ typer.Argument(help="Name of the cluster to deploy"),
233
+ ],
234
+ ) -> None:
235
+ """Deploy a Vantage JupyterHub SLURM cluster using Juju localhost."""
236
+ console = Console()
237
+ console.print(Panel("Juju Localhost SLURM Application"))
238
+ console.print("Deploying juju localhost slurm application...")
239
+ # Get cluster data by name - for now using local import to avoid circular imports
240
+ from vantage_cli.commands.clusters import utils as cluster_utils
241
+
242
+ cluster_data = await cluster_utils.get_cluster_by_name(ctx, cluster_name)
243
+
244
+ # Fallback for testing if cluster not found
245
+ if not cluster_data:
246
+ console.print(
247
+ f"[yellow]Warning: Cluster '{cluster_name}' not found. Using test configuration.[/yellow]"
248
+ )
249
+ cluster_data = {"clientId": "dummy_client_for_testing"}
250
+
251
+ if not cluster_data:
252
+ console.print("[red]Error: No cluster data found.[/red]")
253
+ raise typer.Exit(code=1)
254
+
255
+ await deploy(ctx=ctx, cluster_data=cluster_data)
@@ -0,0 +1,143 @@
1
+ # Copyright (c) 2025 Vantage Compute Corporation
2
+ # See LICENSE file for licensing details.
3
+ """Juju bundle definition for Vantage JupyterHub cluster deployment on localhost."""
4
+
5
+ from typing import Any, Dict
6
+
7
+ VANTAGE_JUPYTERHUB_YAML: Dict[str, Any] = {
8
+ "applications": {
9
+ "jobbergate-agent": {
10
+ "charm": "jobbergate-agent",
11
+ "base": "ubuntu@24.04/stable",
12
+ "channel": "edge",
13
+ "num_units": 0,
14
+ "options": {
15
+ "jobbergate-agent-oidc-client-id": "",
16
+ "jobbergate-agent-oidc-client-secret": "",
17
+ },
18
+ },
19
+ "vantage-agent": {
20
+ "charm": "vantage-agent",
21
+ "base": "ubuntu@24.04/stable",
22
+ "channel": "edge",
23
+ "num_units": 0,
24
+ "options": {
25
+ "vantage-agent-oidc-client-id": "",
26
+ "vantage-agent-oidc-client-secret": "",
27
+ "vantage-agent-cluster-name": "",
28
+ },
29
+ },
30
+ "vantage-jupyterhub-nfs-client": {
31
+ "charm": "filesystem-client",
32
+ "base": "ubuntu@24.04/stable",
33
+ "channel": "latest/edge",
34
+ "num_units": 0,
35
+ "options": {
36
+ "mountpoint": "/srv/vantage-nfs",
37
+ },
38
+ },
39
+ "mysql": {
40
+ "charm": "mysql",
41
+ "base": "ubuntu@22.04/stable",
42
+ "channel": "8.0/stable",
43
+ "num_units": 1,
44
+ "to": ["0"],
45
+ "constraints": "arch=amd64",
46
+ "storage": {
47
+ "database": "rootfs,1,1024M",
48
+ },
49
+ },
50
+ "influxdb": {
51
+ "charm": "influxdb",
52
+ "channel": "stable",
53
+ "base": "ubuntu@20.04/stable",
54
+ "num_units": 1,
55
+ "to": ["1"],
56
+ "constraints": "arch=amd64",
57
+ },
58
+ "slurmdbd": {
59
+ "charm": "slurmdbd",
60
+ "base": "ubuntu@24.04/stable",
61
+ "channel": "latest/edge",
62
+ "num_units": 1,
63
+ "to": ["2"],
64
+ "constraints": "arch=amd64",
65
+ },
66
+ "vantage-jupyterhub": {
67
+ "charm": "vantage-jupyterhub",
68
+ "base": "ubuntu@24.04/stable",
69
+ "channel": "edge",
70
+ "num_units": 1,
71
+ "options": {
72
+ "vantage-jupyterhub-config-secret-id": "",
73
+ },
74
+ "to": ["3"],
75
+ "constraints": "arch=amd64 cpu-cores=2 mem=2048 virt-type=virtual-machine",
76
+ },
77
+ "sackd": {
78
+ "charm": "sackd",
79
+ "base": "ubuntu@24.04/stable",
80
+ "channel": "latest/edge",
81
+ "num_units": 1,
82
+ "to": ["3"],
83
+ "constraints": "arch=amd64 cpu-cores=2 mem=2048 virt-type=virtual-machine",
84
+ },
85
+ "slurmctld": {
86
+ "charm": "slurmctld",
87
+ "base": "ubuntu@24.04/stable",
88
+ "channel": "latest/edge",
89
+ "num_units": 1,
90
+ "to": ["4"],
91
+ "options": {
92
+ "default-partition": "slurmd",
93
+ "cluster-name": "",
94
+ },
95
+ "constraints": "arch=amd64 cpu-cores=2 mem=2048 virt-type=virtual-machine",
96
+ },
97
+ "slurmd": {
98
+ "charm": "slurmd",
99
+ "base": "ubuntu@24.04/stable",
100
+ "channel": "latest/edge",
101
+ "num_units": 1,
102
+ "to": ["5"],
103
+ "constraints": "arch=amd64 cpu-cores=4 mem=8192 virt-type=virtual-machine",
104
+ },
105
+ },
106
+ "machines": {
107
+ "0": {
108
+ "constraints": "arch=amd64",
109
+ "base": "ubuntu@22.04/stable",
110
+ },
111
+ "1": {
112
+ "constraints": "arch=amd64",
113
+ "base": "ubuntu@20.04/stable",
114
+ },
115
+ "2": {
116
+ "constraints": "arch=amd64",
117
+ "base": "ubuntu@24.04/stable",
118
+ },
119
+ "3": {
120
+ "constraints": "arch=amd64 cpu-cores=2 mem=2048 virt-type=virtual-machine",
121
+ "base": "ubuntu@24.04/stable",
122
+ },
123
+ "4": {
124
+ "constraints": "arch=amd64 cpu-cores=2 mem=2048 virt-type=virtual-machine",
125
+ "base": "ubuntu@24.04/stable",
126
+ },
127
+ "5": {
128
+ "constraints": "arch=amd64 cpu-cores=4 mem=8192 virt-type=virtual-machine",
129
+ "base": "ubuntu@24.04/stable",
130
+ },
131
+ },
132
+ "relations": [
133
+ ["slurmdbd:database", "mysql:database"],
134
+ ["slurmctld:influxdb", "influxdb:query"],
135
+ ["slurmdbd:slurmctld", "slurmctld:slurmdbd"],
136
+ ["slurmd:slurmctld", "slurmctld:slurmd"],
137
+ ["sackd:slurmctld", "slurmctld:login-node"],
138
+ ["vantage-jupyterhub-nfs-client:juju-info", "slurmd:juju-info"],
139
+ ["vantage-jupyterhub:filesystem", "vantage-jupyterhub-nfs-client:filesystem"],
140
+ ["sackd:juju-info", "vantage-agent:juju-info"],
141
+ ["sackd:juju-info", "jobbergate-agent:juju-info"],
142
+ ],
143
+ }