vantage6 5.0.0a33__py3-none-any.whl → 5.0.0a35__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.

Potentially problematic release.


This version of vantage6 might be problematic. Click here for more details.

Files changed (45) hide show
  1. vantage6/cli/algostore/new.py +106 -47
  2. vantage6/cli/algostore/remove.py +18 -34
  3. vantage6/cli/algostore/start.py +36 -67
  4. vantage6/cli/algostore/stop.py +43 -46
  5. vantage6/cli/cli.py +31 -33
  6. vantage6/cli/common/new.py +85 -0
  7. vantage6/cli/common/remove.py +54 -0
  8. vantage6/cli/common/start.py +36 -213
  9. vantage6/cli/common/stop.py +78 -0
  10. vantage6/cli/common/utils.py +253 -16
  11. vantage6/cli/configuration_manager.py +90 -12
  12. vantage6/cli/configuration_wizard.py +49 -414
  13. vantage6/cli/context/algorithm_store.py +7 -6
  14. vantage6/cli/context/base_server.py +22 -30
  15. vantage6/cli/context/node.py +14 -17
  16. vantage6/cli/context/server.py +16 -7
  17. vantage6/cli/globals.py +29 -8
  18. vantage6/cli/node/attach.py +1 -0
  19. vantage6/cli/node/common/__init__.py +1 -1
  20. vantage6/cli/node/create_private_key.py +9 -6
  21. vantage6/cli/node/files.py +12 -25
  22. vantage6/cli/node/new.py +348 -28
  23. vantage6/cli/node/remove.py +14 -90
  24. vantage6/cli/node/restart.py +30 -51
  25. vantage6/cli/node/set_api_key.py +7 -4
  26. vantage6/cli/node/start.py +81 -304
  27. vantage6/cli/node/stop.py +36 -96
  28. vantage6/cli/server/import_.py +1 -2
  29. vantage6/cli/server/list.py +0 -3
  30. vantage6/cli/server/new.py +72 -42
  31. vantage6/cli/server/remove.py +12 -33
  32. vantage6/cli/server/shell.py +1 -1
  33. vantage6/cli/server/start.py +22 -20
  34. vantage6/cli/server/stop.py +37 -17
  35. vantage6/cli/template/algo_store_config.j2 +195 -22
  36. vantage6/cli/template/node_config.j2 +336 -33
  37. vantage6/cli/template/server_config.j2 +255 -33
  38. vantage6/cli/utils.py +0 -2
  39. {vantage6-5.0.0a33.dist-info → vantage6-5.0.0a35.dist-info}/METADATA +4 -4
  40. vantage6-5.0.0a35.dist-info/RECORD +75 -0
  41. vantage6/cli/node/clean.py +0 -46
  42. vantage6/cli/template/server_import_config.j2 +0 -31
  43. vantage6-5.0.0a33.dist-info/RECORD +0 -75
  44. {vantage6-5.0.0a33.dist-info → vantage6-5.0.0a35.dist-info}/WHEEL +0 -0
  45. {vantage6-5.0.0a33.dist-info → vantage6-5.0.0a35.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,6 @@
1
- import enum
1
+ import json
2
+ import subprocess
3
+ from pathlib import Path
2
4
  from subprocess import Popen
3
5
  from typing import Iterable
4
6
 
@@ -10,7 +12,175 @@ from colorama import Fore, Style
10
12
  from vantage6.common import error, warning
11
13
  from vantage6.common.globals import APPNAME, STRING_ENCODING, InstanceType
12
14
 
15
+ from vantage6.cli.config import CliConfig
13
16
  from vantage6.cli.context import select_context_class
17
+ from vantage6.cli.globals import CLICommandName
18
+ from vantage6.cli.utils import validate_input_cmd_args
19
+
20
+
21
+ def select_context_and_namespace(
22
+ context: str | None = None,
23
+ namespace: str | None = None,
24
+ ) -> tuple[str, str]:
25
+ """
26
+ Select the context and namespace to use.
27
+
28
+ This uses the CLI config to compare the provided context and namespace with the
29
+ last used context and namespace. If the provided context and namespace are not
30
+ the same as the last used context and namespace, the CLI config is updated.
31
+
32
+ Parameters
33
+ ----------
34
+ context : str, optional
35
+ The Kubernetes context to use.
36
+ namespace : str, optional
37
+ The Kubernetes namespace to use.
38
+
39
+ Returns
40
+ -------
41
+ tuple[str, str]
42
+ The context and namespace to use
43
+ """
44
+ cli_config = CliConfig()
45
+
46
+ return cli_config.compare_changes_config(
47
+ context=context,
48
+ namespace=namespace,
49
+ )
50
+
51
+
52
+ def create_directory_if_not_exists(directory: Path) -> None:
53
+ """
54
+ Create a directory.
55
+ """
56
+ try:
57
+ directory.mkdir(parents=True, exist_ok=True)
58
+ except Exception as e:
59
+ error(f"Failed to create directory {directory}: {e}")
60
+ exit(1)
61
+
62
+
63
+ def find_running_service_names(
64
+ instance_type: InstanceType,
65
+ only_system_folders: bool = False,
66
+ only_user_folders: bool = False,
67
+ context: str | None = None,
68
+ namespace: str | None = None,
69
+ ) -> list[str]:
70
+ """
71
+ List running Vantage6 servers.
72
+
73
+ Parameters
74
+ ----------
75
+ instance_type : InstanceType
76
+ The type of instance to find running services for
77
+ only_system_folders : bool, optional
78
+ Whether to look for system-based services or not. By default False.
79
+ only_user_folders : bool, optional
80
+ Whether to look for user-based services or not. By default False.
81
+ context : str, optional
82
+ The Kubernetes context to use.
83
+ namespace : str, optional
84
+ The Kubernetes namespace to use.
85
+
86
+ Returns
87
+ -------
88
+ list[str]
89
+ List of release names that are running
90
+ """
91
+ # Input validation
92
+ validate_input_cmd_args(context, "context name", allow_none=True)
93
+ validate_input_cmd_args(namespace, "namespace name", allow_none=True)
94
+ validate_input_cmd_args(instance_type, "instance type", allow_none=False)
95
+ if only_system_folders and only_user_folders:
96
+ error("Cannot use both only_system_folders and only_user_folders")
97
+ exit(1)
98
+
99
+ # Create the command
100
+ command = [
101
+ "helm",
102
+ "list",
103
+ "--output",
104
+ "json", # Get structured output
105
+ ]
106
+
107
+ if context:
108
+ command.extend(["--kube-context", context])
109
+
110
+ if namespace:
111
+ command.extend(["--namespace", namespace])
112
+ else:
113
+ command.extend(["--all-namespaces"])
114
+
115
+ try:
116
+ result = subprocess.run(
117
+ command,
118
+ capture_output=True,
119
+ text=True,
120
+ check=True,
121
+ )
122
+ except subprocess.CalledProcessError as e:
123
+ error(f"Failed to list Helm releases: {e}")
124
+ return []
125
+ except FileNotFoundError:
126
+ error(
127
+ "Helm command not found. Please ensure Helm is installed and available in "
128
+ "the PATH."
129
+ )
130
+ return []
131
+
132
+ try:
133
+ releases = json.loads(result.stdout)
134
+ except json.JSONDecodeError:
135
+ error("Failed to parse Helm output as JSON")
136
+ return []
137
+
138
+ # filter services for the vantage6 services that are sought. These have
139
+ # the following pattern:
140
+ # f"{APPNAME}-{name}-{scope}-{instance_type.value}"
141
+
142
+ # filter for the instance type
143
+ svc_starts_with = f"{APPNAME}-"
144
+ if only_system_folders:
145
+ svc_ends_with = f"system-{instance_type.value}"
146
+ elif only_user_folders:
147
+ svc_ends_with = f"user-{instance_type.value}"
148
+ else:
149
+ svc_ends_with = f"-{instance_type.value}"
150
+
151
+ matching_services = []
152
+ for release in releases:
153
+ release_name = release.get("name", "")
154
+
155
+ # Check if this is a Vantage6 server release
156
+ is_matching_service = (
157
+ release_name.startswith(svc_starts_with)
158
+ and release_name.endswith(svc_ends_with)
159
+ and instance_type.value in release_name
160
+ )
161
+
162
+ if is_matching_service:
163
+ matching_services.append(release_name)
164
+
165
+ return matching_services
166
+
167
+
168
+ def select_running_service(
169
+ running_services: list[str],
170
+ instance_type: InstanceType,
171
+ ) -> str:
172
+ """
173
+ Select a running service from the list of running services.
174
+ """
175
+ try:
176
+ name = q.select(
177
+ f"Select a {instance_type.value}:",
178
+ choices=running_services,
179
+ ).unsafe_ask()
180
+ except KeyboardInterrupt:
181
+ error("Aborted by user!")
182
+ exit(1)
183
+ return name
14
184
 
15
185
 
16
186
  def get_server_name(
@@ -20,30 +190,30 @@ def get_server_name(
20
190
  instance_type: InstanceType,
21
191
  ) -> str:
22
192
  """
23
- Get the version of a running server.
193
+ Get the full name of a running server.
24
194
 
25
195
  Parameters
26
196
  ----------
27
197
  name : str
28
- Name of the server to get the version from
198
+ Name of the server to get the full name from
29
199
  system_folders : bool
30
200
  Whether to use system folders or not
31
201
  running_server_names : list[str]
32
202
  The names of the running servers
33
203
  instance_type : InstanceType
34
- The type of instance to get the running servers from
204
+ The type of instance to get the full name from
35
205
  """
36
206
 
37
207
  if not name:
38
208
  if not running_server_names:
39
209
  error(
40
- f"No {instance_type}s are running! You can only check the version for "
41
- f"{instance_type}s that are running"
210
+ f"No {instance_type.value}s are running! You can only check the version"
211
+ f" for {instance_type.value}s that are running"
42
212
  )
43
213
  exit(1)
44
214
  try:
45
215
  name = q.select(
46
- f"Select the {instance_type} you wish to inspect:",
216
+ f"Select the {instance_type.value} you wish to inspect:",
47
217
  choices=running_server_names,
48
218
  ).unsafe_ask()
49
219
  except KeyboardInterrupt:
@@ -73,7 +243,7 @@ def get_running_servers(
73
243
  The names of the running servers
74
244
  """
75
245
  running_servers = client.containers.list(
76
- filters={"label": f"{APPNAME}-type={instance_type}"}
246
+ filters={"label": f"{APPNAME}-type={instance_type.value}"}
77
247
  )
78
248
  return [server.name for server in running_servers]
79
249
 
@@ -87,10 +257,9 @@ def get_server_configuration_list(instance_type: InstanceType) -> None:
87
257
  instance_type : InstanceType
88
258
  The type of instance to get the configurations for
89
259
  """
90
- client = docker.from_env()
91
260
  ctx_class = select_context_class(instance_type)
92
261
 
93
- running_server_names = get_running_servers(client, instance_type)
262
+ running_server_names = find_running_service_names(instance_type)
94
263
  header = "\nName" + (21 * " ") + "Status" + (10 * " ") + "System/User"
95
264
 
96
265
  click.echo(header)
@@ -100,28 +269,37 @@ def get_server_configuration_list(instance_type: InstanceType) -> None:
100
269
  stopped = Fore.RED + "Not running" + Style.RESET_ALL
101
270
 
102
271
  # system folders
103
- configs, f1 = ctx_class.available_configurations(system_folders=True)
272
+ configs, failed_imports_system = ctx_class.available_configurations(
273
+ system_folders=True
274
+ )
104
275
  for config in configs:
105
276
  status = (
106
277
  running
107
- if f"{APPNAME}-{config.name}-system-{instance_type}" in running_server_names
278
+ if f"{APPNAME}-{config.name}-system-{instance_type.value}"
279
+ in running_server_names
108
280
  else stopped
109
281
  )
110
282
  click.echo(f"{config.name:25}{status:25} System ")
111
283
 
112
284
  # user folders
113
- configs, f2 = ctx_class.available_configurations(system_folders=False)
285
+ configs, failed_imports_user = ctx_class.available_configurations(
286
+ system_folders=False
287
+ )
114
288
  for config in configs:
115
289
  status = (
116
290
  running
117
- if f"{APPNAME}-{config.name}-user-{instance_type}" in running_server_names
291
+ if f"{APPNAME}-{config.name}-user-{instance_type.value}"
292
+ in running_server_names
118
293
  else stopped
119
294
  )
120
295
  click.echo(f"{config.name:25}{status:25} User ")
121
296
 
122
297
  click.echo("-" * 85)
123
- if len(f1) + len(f2):
124
- warning(f"{Fore.RED}Failed imports: {len(f1) + len(f2)}{Style.RESET_ALL}")
298
+ if len(failed_imports_system) + len(failed_imports_user):
299
+ warning(
300
+ f"{Fore.RED}Failed imports: "
301
+ f"{len(failed_imports_system) + len(failed_imports_user)}{Style.RESET_ALL}"
302
+ )
125
303
 
126
304
 
127
305
  def print_log_worker(logs_stream: Iterable[bytes]) -> None:
@@ -163,6 +341,16 @@ def get_name_from_container_name(container_name: str) -> str:
163
341
  return "-".join(container_name.split("-")[1:-1])
164
342
 
165
343
 
344
+ def get_config_name_from_service_name(service_name: str) -> str:
345
+ """
346
+ Get the config name from a service name.
347
+ """
348
+ # helm release name is structured as:
349
+ # f"{APPNAME}-{name}-{scope}-{instance_type}"
350
+ # we want to get the name from the service name
351
+ return "-".join(service_name.split("-")[1:-2])
352
+
353
+
166
354
  def attach_logs(*labels: list[str]) -> None:
167
355
  """
168
356
  Attach to the logs of the given labels.
@@ -175,3 +363,52 @@ def attach_logs(*labels: list[str]) -> None:
175
363
  command = ["kubectl", "logs", "--follow", "--selector", ",".join(labels)]
176
364
  process = Popen(command, stdout=None, stderr=None)
177
365
  process.wait()
366
+
367
+
368
+ def get_main_cli_command_name(instance_type: InstanceType) -> str:
369
+ """
370
+ Get the main CLI command name for a given instance type.
371
+
372
+ Parameters
373
+ ----------
374
+ instance_type : InstanceType
375
+ The type of instance to get the main CLI command name for
376
+ """
377
+ if instance_type == InstanceType.SERVER:
378
+ return CLICommandName.SERVER.value
379
+ elif instance_type == InstanceType.ALGORITHM_STORE:
380
+ return CLICommandName.ALGORITHM_STORE.value
381
+ elif instance_type == InstanceType.NODE:
382
+ return CLICommandName.NODE.value
383
+ else:
384
+ raise ValueError(f"Invalid instance type: {instance_type}")
385
+
386
+
387
+ def check_running(
388
+ helm_release_name: str, instance_type: InstanceType, name: str, system_folders: bool
389
+ ) -> bool:
390
+ """
391
+ Check if the instance is already running.
392
+
393
+ Parameters
394
+ ----------
395
+ helm_release_name : str
396
+ The name of the Helm release.
397
+ instance_type : InstanceType
398
+ The type of instance to check
399
+ name : str
400
+ The name of the instance to check
401
+ system_folders : bool
402
+ Whether to use system folders or not
403
+
404
+ Returns
405
+ -------
406
+ bool
407
+ True if the instance is already running, False otherwise
408
+ """
409
+ running_services = find_running_service_names(
410
+ instance_type=instance_type,
411
+ only_system_folders=system_folders,
412
+ only_user_folders=not system_folders,
413
+ )
414
+ return helm_release_name in running_services
@@ -1,9 +1,15 @@
1
1
  from typing import Self
2
2
 
3
- from schema import And, Optional, Or, Use
3
+ from schema import And, Use
4
4
 
5
5
  from vantage6.common.configuration_manager import Configuration, ConfigurationManager
6
6
 
7
+ from vantage6.cli.globals import (
8
+ ALGO_STORE_TEMPLATE_FILE,
9
+ NODE_TEMPLATE_FILE,
10
+ SERVER_TEMPLATE_FILE,
11
+ )
12
+
7
13
  LOGGING_VALIDATORS = {
8
14
  "level": And(
9
15
  Use(str), lambda lvl: lvl in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
@@ -26,6 +32,15 @@ class ServerConfiguration(Configuration):
26
32
  VALIDATORS = {}
27
33
 
28
34
 
35
+ class AlgorithmStoreConfiguration(Configuration):
36
+ """
37
+ Stores the algorithm store's configuration and defines a set of algorithm store-specific
38
+ validators.
39
+ """
40
+
41
+ VALIDATORS = {}
42
+
43
+
29
44
  class NodeConfiguration(Configuration):
30
45
  """
31
46
  Stores the node's configuration and defines a set of node-specific
@@ -33,17 +48,20 @@ class NodeConfiguration(Configuration):
33
48
  """
34
49
 
35
50
  VALIDATORS = {
36
- "server_url": Use(str),
37
- "port": Or(Use(int), None),
38
- "task_dir": Use(str),
39
- # TODO: remove `dict` validation from databases
40
- "api_path": Use(str),
41
- "logging": LOGGING_VALIDATORS,
42
- "encryption": {"enabled": bool, Optional("private_key"): Use(str)},
43
- Optional("node_extra_env"): dict,
44
- Optional("node_extra_mounts"): [str],
45
- Optional("node_extra_hosts"): dict,
46
- Optional("share_algorithm_logs"): Use(bool),
51
+ # # TODO enable validators for node. To see if it works, use v6 node list
52
+ # "node": {
53
+ # "server_url": Use(str),
54
+ # "port": Or(Use(int), None),
55
+ # "task_dir": Use(str),
56
+ # # TODO: remove `dict` validation from databases
57
+ # "api_path": Use(str),
58
+ # "logging": LOGGING_VALIDATORS,
59
+ # "encryption": {"enabled": bool, Optional("private_key"): Use(str)},
60
+ # Optional("node_extra_env"): dict,
61
+ # Optional("node_extra_mounts"): [str],
62
+ # Optional("node_extra_hosts"): dict,
63
+ # Optional("share_algorithm_logs"): Use(bool),
64
+ # }
47
65
  }
48
66
 
49
67
 
@@ -82,6 +100,12 @@ class NodeConfigurationManager(ConfigurationManager):
82
100
  """
83
101
  return super().from_file(path, conf_class=NodeConfiguration)
84
102
 
103
+ def get_config_template(self) -> str:
104
+ """
105
+ Get the configuration template for the node.
106
+ """
107
+ return super()._get_config_template(NODE_TEMPLATE_FILE)
108
+
85
109
 
86
110
  class ServerConfigurationManager(ConfigurationManager):
87
111
  """
@@ -114,6 +138,60 @@ class ServerConfigurationManager(ConfigurationManager):
114
138
  """
115
139
  return super().from_file(path, conf_class=ServerConfiguration)
116
140
 
141
+ def get_config_template(self) -> str:
142
+ """
143
+ Get the configuration template for the server.
144
+
145
+ Returns
146
+ -------
147
+ str
148
+ The configuration template for the server.
149
+ """
150
+ return super()._get_config_template(SERVER_TEMPLATE_FILE)
151
+
152
+
153
+ class AlgorithmStoreConfigurationManager(ConfigurationManager):
154
+ """
155
+ Maintains the algorithm store's configuration.
156
+
157
+ Parameters
158
+ ----------
159
+ name : str
160
+ Name of the configuration file.
161
+ """
162
+
163
+ def __init__(self, name, *args, **kwargs) -> None:
164
+ super().__init__(conf_class=AlgorithmStoreConfiguration, name=name)
165
+
166
+ @classmethod
167
+ def from_file(cls, path: str) -> Self:
168
+ """
169
+ Create a new instance of the AlgorithmStoreConfigurationManager from a
170
+ configuration file.
171
+
172
+ Parameters
173
+ ----------
174
+ path : str
175
+ Path to the configuration file.
176
+
177
+ Returns
178
+ -------
179
+ AlgorithmStoreConfigurationManager
180
+ A new instance of the AlgorithmStoreConfigurationManager.
181
+ """
182
+ return super().from_file(path, conf_class=AlgorithmStoreConfiguration)
183
+
184
+ def get_config_template(self) -> str:
185
+ """
186
+ Get the configuration template for the algorithm store.
187
+
188
+ Returns
189
+ -------
190
+ str
191
+ The configuration template for the algorithm store.
192
+ """
193
+ return super()._get_config_template(ALGO_STORE_TEMPLATE_FILE)
194
+
117
195
 
118
196
  class TestingConfigurationManager(ConfigurationManager):
119
197
  def __init__(self, name, *args, **kwargs):