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.
- vantage_cli/__init__.py +131 -0
- vantage_cli/apps/__init__.py +22 -0
- vantage_cli/apps/common.py +78 -0
- vantage_cli/apps/juju_localhost/__init__.py +17 -0
- vantage_cli/apps/juju_localhost/app.py +255 -0
- vantage_cli/apps/juju_localhost/bundle_yaml.py +143 -0
- vantage_cli/apps/microk8s/README.md +47 -0
- vantage_cli/apps/microk8s/__init__.py +3 -0
- vantage_cli/apps/microk8s/app.py +301 -0
- vantage_cli/apps/multipass_singlenode/__init__.py +12 -0
- vantage_cli/apps/multipass_singlenode/app.py +173 -0
- vantage_cli/apps/templates.py +178 -0
- vantage_cli/auth.py +429 -0
- vantage_cli/cache.py +143 -0
- vantage_cli/client.py +84 -0
- vantage_cli/command_base.py +63 -0
- vantage_cli/commands/__init__.py +1 -0
- vantage_cli/commands/clouds/__init__.py +20 -0
- vantage_cli/commands/clouds/add.py +81 -0
- vantage_cli/commands/clouds/delete.py +61 -0
- vantage_cli/commands/clouds/render.py +146 -0
- vantage_cli/commands/clouds/update.py +97 -0
- vantage_cli/commands/clusters/__init__.py +27 -0
- vantage_cli/commands/clusters/create.py +270 -0
- vantage_cli/commands/clusters/delete.py +101 -0
- vantage_cli/commands/clusters/get.py +30 -0
- vantage_cli/commands/clusters/list.py +84 -0
- vantage_cli/commands/clusters/render.py +233 -0
- vantage_cli/commands/clusters/schema.py +31 -0
- vantage_cli/commands/clusters/utils.py +248 -0
- vantage_cli/commands/profile/__init__.py +30 -0
- vantage_cli/commands/profile/crud.py +529 -0
- vantage_cli/commands/profile/render.py +55 -0
- vantage_cli/config.py +161 -0
- vantage_cli/constants.py +40 -0
- vantage_cli/exceptions.py +127 -0
- vantage_cli/format.py +39 -0
- vantage_cli/gql_client.py +655 -0
- vantage_cli/main.py +303 -0
- vantage_cli/render.py +56 -0
- vantage_cli/schemas.py +48 -0
- vantage_cli/time_loop.py +124 -0
- vantage_cli-0.1.1.dist-info/METADATA +30 -0
- vantage_cli-0.1.1.dist-info/RECORD +46 -0
- vantage_cli-0.1.1.dist-info/WHEEL +4 -0
- vantage_cli-0.1.1.dist-info/entry_points.txt +2 -0
vantage_cli/__init__.py
ADDED
|
@@ -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
|
+
}
|