vantage6 5.0.0a0__py3-none-any.whl → 5.0.0a9__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. tests_cli/test_node_cli.py +36 -25
  2. tests_cli/test_server_cli.py +33 -12
  3. tests_cli/test_wizard.py +5 -5
  4. vantage6/cli/__build__ +1 -1
  5. vantage6/cli/__init__.py +1 -1
  6. vantage6/cli/algorithm/create.py +26 -8
  7. vantage6/cli/algorithm/update.py +5 -1
  8. vantage6/cli/algostore/attach.py +6 -25
  9. vantage6/cli/algostore/new.py +9 -3
  10. vantage6/cli/cli.py +6 -11
  11. vantage6/cli/common/start.py +1 -1
  12. vantage6/cli/common/utils.py +42 -4
  13. vantage6/cli/configuration_wizard.py +63 -51
  14. vantage6/cli/context/node.py +18 -1
  15. vantage6/cli/node/attach.py +5 -62
  16. vantage6/cli/node/clean.py +6 -2
  17. vantage6/cli/node/common/__init__.py +26 -4
  18. vantage6/cli/node/create_private_key.py +10 -2
  19. vantage6/cli/node/new.py +7 -3
  20. vantage6/cli/node/restart.py +128 -0
  21. vantage6/cli/node/set_api_key.py +7 -3
  22. vantage6/cli/node/start.py +12 -4
  23. vantage6/cli/node/stop.py +7 -3
  24. vantage6/cli/node/version.py +7 -3
  25. vantage6/cli/server/attach.py +5 -51
  26. vantage6/cli/server/new.py +7 -3
  27. vantage6/cli/server/start.py +1 -1
  28. vantage6/cli/server/stop.py +7 -3
  29. vantage6/cli/template/node_config.j2 +3 -1
  30. vantage6/cli/test/common/diagnostic_runner.py +2 -4
  31. vantage6/cli/test/feature_tester.py +10 -3
  32. vantage6/cli/test/integration_test.py +113 -113
  33. vantage6/cli/utils.py +5 -1
  34. {vantage6-5.0.0a0.dist-info → vantage6-5.0.0a9.dist-info}/METADATA +5 -5
  35. vantage6-5.0.0a9.dist-info/RECORD +70 -0
  36. vantage6/cli/dev/create.py +0 -632
  37. vantage6/cli/dev/data/olympic_athletes_2016.csv +0 -2425
  38. vantage6/cli/dev/remove.py +0 -94
  39. vantage6/cli/dev/start.py +0 -123
  40. vantage6/cli/dev/stop.py +0 -47
  41. vantage6/cli/dev/utils.py +0 -24
  42. vantage6-5.0.0a0.dist-info/RECORD +0 -75
  43. {vantage6-5.0.0a0.dist-info → vantage6-5.0.0a9.dist-info}/WHEEL +0 -0
  44. {vantage6-5.0.0a0.dist-info → vantage6-5.0.0a9.dist-info}/entry_points.txt +0 -0
  45. {vantage6-5.0.0a0.dist-info → vantage6-5.0.0a9.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
- import questionary as q
2
-
3
1
  from pathlib import Path
4
2
 
3
+ import questionary as q
4
+
5
5
  from vantage6.common import generate_apikey
6
6
  from vantage6.common.globals import (
7
7
  DATABASE_TYPES,
@@ -36,7 +36,7 @@ def node_configuration_questionaire(dirs: dict, instance_name: str) -> dict:
36
36
  dict
37
37
  Dictionary with the new node configuration
38
38
  """
39
- config = q.prompt(
39
+ config = q.unsafe_prompt(
40
40
  [
41
41
  {"type": "text", "name": "api_key", "message": "Enter given api-key:"},
42
42
  {
@@ -57,7 +57,7 @@ def node_configuration_questionaire(dirs: dict, instance_name: str) -> dict:
57
57
  else str(Ports.DEV_SERVER.value)
58
58
  )
59
59
 
60
- config = config | q.prompt(
60
+ config = config | q.unsafe_prompt(
61
61
  [
62
62
  {
63
63
  "type": "text",
@@ -81,8 +81,8 @@ def node_configuration_questionaire(dirs: dict, instance_name: str) -> dict:
81
81
  )
82
82
 
83
83
  config["databases"] = list()
84
- while q.confirm("Do you want to add a database?").ask():
85
- db_label = q.prompt(
84
+ while q.confirm("Do you want to add a database?").unsafe_ask():
85
+ db_label = q.unsafe_prompt(
86
86
  [
87
87
  {
88
88
  "type": "text",
@@ -92,10 +92,10 @@ def node_configuration_questionaire(dirs: dict, instance_name: str) -> dict:
92
92
  }
93
93
  ]
94
94
  )
95
- db_path = q.prompt(
95
+ db_path = q.unsafe_prompt(
96
96
  [{"type": "text", "name": "uri", "message": "Database URI:"}]
97
97
  )
98
- db_type = q.select("Database type:", choices=DATABASE_TYPES).ask()
98
+ db_type = q.select("Database type:", choices=DATABASE_TYPES).unsafe_ask()
99
99
 
100
100
  config["databases"].append(
101
101
  {"label": db_label.get("label"), "uri": db_path.get("uri"), "type": db_type}
@@ -103,18 +103,18 @@ def node_configuration_questionaire(dirs: dict, instance_name: str) -> dict:
103
103
 
104
104
  is_add_vpn = q.confirm(
105
105
  "Do you want to connect to a VPN server?", default=False
106
- ).ask()
106
+ ).unsafe_ask()
107
107
  if is_add_vpn:
108
108
  config["vpn_subnet"] = q.text(
109
109
  message="Subnet of the VPN server you want to connect to:",
110
110
  default="10.76.0.0/16",
111
- ).ask()
111
+ ).unsafe_ask()
112
112
 
113
113
  is_policies = q.confirm(
114
114
  "Do you want to limit the algorithms allowed to run on your node? This "
115
115
  "should always be done for production scenarios.",
116
116
  default=True,
117
- ).ask()
117
+ ).unsafe_ask()
118
118
  policies = {}
119
119
  if is_policies:
120
120
  info(
@@ -124,12 +124,12 @@ def node_configuration_questionaire(dirs: dict, instance_name: str) -> dict:
124
124
  )
125
125
  ask_single_algorithms = q.confirm(
126
126
  "Do you want to enter a list of allowed algorithms?"
127
- ).ask()
127
+ ).unsafe_ask()
128
128
  if ask_single_algorithms:
129
129
  policies[NodePolicy.ALLOWED_ALGORITHMS.value] = _get_allowed_algorithms()
130
130
  ask_algorithm_stores = q.confirm(
131
131
  "Do you want to allow algorithms from specific algorithm stores?"
132
- ).ask()
132
+ ).unsafe_ask()
133
133
  if ask_algorithm_stores:
134
134
  policies[NodePolicy.ALLOWED_ALGORITHM_STORES.value] = (
135
135
  _get_allowed_algorithm_stores()
@@ -141,7 +141,7 @@ def node_configuration_questionaire(dirs: dict, instance_name: str) -> dict:
141
141
  "stores? If not, algorithms will be allowed if they are in either the "
142
142
  "list of allowed algorithms or one of the allowed algorithm stores.",
143
143
  default=True,
144
- ).ask()
144
+ ).unsafe_ask()
145
145
  policies["allow_either_whitelist_or_store"] = not require_both_whitelists
146
146
  if policies:
147
147
  config["policies"] = policies
@@ -149,7 +149,7 @@ def node_configuration_questionaire(dirs: dict, instance_name: str) -> dict:
149
149
  res = q.select(
150
150
  "Which level of logging would you like?",
151
151
  choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NOTSET"],
152
- ).ask()
152
+ ).unsafe_ask()
153
153
 
154
154
  config["logging"] = {
155
155
  "level": res,
@@ -178,7 +178,7 @@ def node_configuration_questionaire(dirs: dict, instance_name: str) -> dict:
178
178
  "If you continue, you should provide your collaboration "
179
179
  "settings manually."
180
180
  )
181
- if q.confirm("Do you want to abort?", default=True).ask():
181
+ if q.confirm("Do you want to abort?", default=True).unsafe_ask():
182
182
  exit(0)
183
183
 
184
184
  if client.whoami is not None:
@@ -190,11 +190,13 @@ def node_configuration_questionaire(dirs: dict, instance_name: str) -> dict:
190
190
  f"Encryption is {'enabled' if encryption else 'disabled'}"
191
191
  f" for this collaboration. Accept?",
192
192
  default=True,
193
- ).ask()
193
+ ).unsafe_ask()
194
194
  else:
195
- encryption = q.confirm("Enable encryption?", default=True).ask()
195
+ encryption = q.confirm("Enable encryption?", default=True).unsafe_ask()
196
196
 
197
- private_key = "" if not encryption else q.text("Path to private key file:").ask()
197
+ private_key = (
198
+ "" if not encryption else q.text("Path to private key file:").unsafe_ask()
199
+ )
198
200
 
199
201
  config["encryption"] = {
200
202
  "enabled": encryption is True or encryption == "true",
@@ -219,24 +221,22 @@ def _get_allowed_algorithms() -> list[str]:
219
221
  "use strings to provide one algorithm at a time."
220
222
  )
221
223
  info("Examples:")
222
- # pylint: disable=W1401
223
- # flake8: noqa: W605
224
- info("^harbor2\.vantage6\.ai/demo/average$ Allow the demo average algorithm")
224
+ info(r"^harbor2\.vantage6\.ai/demo/average$ Allow the demo average algorithm")
225
225
  info(
226
- "^harbor2\.vantage6\.ai/algorithms/.* Allow all algorithms from "
226
+ r"^harbor2\.vantage6\.ai/algorithms/.* Allow all algorithms from "
227
227
  "harbor2.vantage6.ai/algorithms"
228
228
  )
229
229
  info(
230
- "^harbor2\.vantage6\.ai/demo/average@sha256:82becede...$ Allow a "
230
+ r"^harbor2\.vantage6\.ai/demo/average@sha256:82becede...$ Allow a "
231
231
  "specific hash of average algorithm"
232
232
  )
233
233
  allowed_algorithms = []
234
234
  while True:
235
- algo = q.text(message="Enter your algorithm expression:").ask()
235
+ algo = q.text(message="Enter your algorithm expression:").unsafe_ask()
236
236
  allowed_algorithms.append(algo)
237
237
  if not q.confirm(
238
238
  "Do you want to add another algorithm expression?", default=True
239
- ).ask():
239
+ ).unsafe_ask():
240
240
  break
241
241
  return allowed_algorithms
242
242
 
@@ -261,16 +261,16 @@ def _get_allowed_algorithm_stores() -> list[str]:
261
261
  "community store"
262
262
  )
263
263
  info(
264
- "^https://*\.vantage6\.ai$ Allow all algorithms from any "
264
+ r"^https://*\.vantage6\.ai$ Allow all algorithms from any "
265
265
  "store hosted on vantage6.ai"
266
266
  )
267
267
  allowed_algorithm_stores = []
268
268
  while True:
269
- store = q.text(message="Enter the URL of the algorithm store:").ask()
269
+ store = q.text(message="Enter the URL of the algorithm store:").unsafe_ask()
270
270
  allowed_algorithm_stores.append(store)
271
271
  if not q.confirm(
272
272
  "Do you want to add another algorithm store?", default=True
273
- ).ask():
273
+ ).unsafe_ask():
274
274
  break
275
275
  return allowed_algorithm_stores
276
276
 
@@ -292,7 +292,7 @@ def _get_common_server_config(instance_type: InstanceType, instance_name: str) -
292
292
  dict
293
293
  Dictionary with new (partial) server configuration
294
294
  """
295
- config = q.prompt(
295
+ config = q.unsafe_prompt(
296
296
  [
297
297
  {
298
298
  "type": "text",
@@ -315,7 +315,7 @@ def _get_common_server_config(instance_type: InstanceType, instance_name: str) -
315
315
  )
316
316
 
317
317
  config.update(
318
- q.prompt(
318
+ q.unsafe_prompt(
319
319
  [
320
320
  {
321
321
  "type": "text",
@@ -336,7 +336,7 @@ def _get_common_server_config(instance_type: InstanceType, instance_name: str) -
336
336
  res = q.select(
337
337
  "Which level of logging would you like?",
338
338
  choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NOTSET"],
339
- ).ask()
339
+ ).unsafe_ask()
340
340
 
341
341
  config["logging"] = {
342
342
  "level": res,
@@ -378,11 +378,11 @@ def server_configuration_questionaire(instance_name: str) -> dict:
378
378
 
379
379
  config = _get_common_server_config(InstanceType.SERVER, instance_name)
380
380
 
381
- constant_jwt_secret = q.confirm("Do you want a constant JWT secret?").ask()
381
+ constant_jwt_secret = q.confirm("Do you want a constant JWT secret?").unsafe_ask()
382
382
  if constant_jwt_secret:
383
383
  config["jwt_secret_key"] = generate_apikey()
384
384
 
385
- is_mfa = q.confirm("Do you want to enforce two-factor authentication?").ask()
385
+ is_mfa = q.confirm("Do you want to enforce two-factor authentication?").unsafe_ask()
386
386
  if is_mfa:
387
387
  config["two_factor_auth"] = is_mfa
388
388
 
@@ -393,11 +393,13 @@ def server_configuration_questionaire(instance_name: str) -> dict:
393
393
  "server url. If you are running a production server, this is the "
394
394
  "url that users will connect to.",
395
395
  default=current_server_url,
396
- ).ask()
396
+ ).unsafe_ask()
397
397
 
398
- is_add_vpn = q.confirm("Do you want to add a VPN server?", default=False).ask()
398
+ is_add_vpn = q.confirm(
399
+ "Do you want to add a VPN server?", default=False
400
+ ).unsafe_ask()
399
401
  if is_add_vpn:
400
- vpn_config = q.prompt(
402
+ vpn_config = q.unsafe_prompt(
401
403
  [
402
404
  {
403
405
  "type": "text",
@@ -434,12 +436,14 @@ def server_configuration_questionaire(instance_name: str) -> dict:
434
436
  )
435
437
  config["vpn_server"] = vpn_config
436
438
 
437
- is_add_rabbitmq = q.confirm("Do you want to add a RabbitMQ message queue?").ask()
439
+ is_add_rabbitmq = q.confirm(
440
+ "Do you want to add a RabbitMQ message queue?"
441
+ ).unsafe_ask()
438
442
  if is_add_rabbitmq:
439
- rabbit_uri = q.text(message="Enter the URI for your RabbitMQ:").ask()
443
+ rabbit_uri = q.text(message="Enter the URI for your RabbitMQ:").unsafe_ask()
440
444
  run_rabbit_locally = q.confirm(
441
445
  "Do you want to run RabbitMQ locally? (Use only for testing)"
442
- ).ask()
446
+ ).unsafe_ask()
443
447
  config["rabbitmq"] = {
444
448
  "uri": rabbit_uri,
445
449
  "start_with_server": run_rabbit_locally,
@@ -449,7 +453,7 @@ def server_configuration_questionaire(instance_name: str) -> dict:
449
453
  is_add_community_store = q.confirm(
450
454
  "Do you want to make the algorithms from the community algorithm store "
451
455
  "available to your users?"
452
- ).ask()
456
+ ).unsafe_ask()
453
457
  algorithm_stores = []
454
458
  if is_add_community_store:
455
459
  algorithm_stores.append(
@@ -457,14 +461,14 @@ def server_configuration_questionaire(instance_name: str) -> dict:
457
461
  )
458
462
  add_more_stores = q.confirm(
459
463
  "Do you want to add more algorithm stores?", default=False
460
- ).ask()
464
+ ).unsafe_ask()
461
465
  while add_more_stores:
462
- store_name = q.text(message="Enter the name of the store:").ask()
463
- store_url = q.text(message="Enter the URL of the store:").ask()
466
+ store_name = q.text(message="Enter the name of the store:").unsafe_ask()
467
+ store_url = q.text(message="Enter the URL of the store:").unsafe_ask()
464
468
  algorithm_stores.append({"name": store_name, "url": store_url})
465
469
  add_more_stores = q.confirm(
466
470
  "Do you want to add more algorithm stores?", default=False
467
- ).ask()
471
+ ).unsafe_ask()
468
472
  config["algorithm_stores"] = algorithm_stores
469
473
 
470
474
  return config
@@ -487,19 +491,21 @@ def algo_store_configuration_questionaire(instance_name: str) -> dict:
487
491
  """
488
492
  config = _get_common_server_config(InstanceType.ALGORITHM_STORE, instance_name)
489
493
 
490
- default_v6_server_uri = f"http://localhost:{Ports.DEV_SERVER.value}/api"
494
+ default_v6_server_uri = (
495
+ f"http://localhost:{Ports.DEV_SERVER.value}{DEFAULT_API_PATH}"
496
+ )
491
497
  default_root_username = "root"
492
498
 
493
499
  v6_server_uri = q.text(
494
500
  "What is the Vantage6 server linked to the algorithm store? "
495
501
  "Provide the link to the server endpoint.",
496
502
  default=default_v6_server_uri,
497
- ).ask()
503
+ ).unsafe_ask()
498
504
 
499
505
  root_username = q.text(
500
506
  "What is the username of the root user?",
501
507
  default=default_root_username,
502
- ).ask()
508
+ ).unsafe_ask()
503
509
 
504
510
  config["root_user"] = {
505
511
  "v6_server_uri": v6_server_uri,
@@ -512,7 +518,7 @@ def algo_store_configuration_questionaire(instance_name: str) -> dict:
512
518
  "Do you want to open the algorithm store to the public? This will allow anyone "
513
519
  "to view the algorithms, but they cannot modify them.",
514
520
  default=False,
515
- ).ask()
521
+ ).unsafe_ask()
516
522
  if is_open:
517
523
  open_algos_policy = "public"
518
524
  else:
@@ -521,7 +527,7 @@ def algo_store_configuration_questionaire(instance_name: str) -> dict:
521
527
  "the algorithms in the store? If not allowing this, you will have to assign"
522
528
  " the appropriate permissions to each user individually.",
523
529
  default=True,
524
- ).ask()
530
+ ).unsafe_ask()
525
531
  open_algos_policy = "whitelisted" if is_open_to_whitelist else "private"
526
532
  config["policies"]["algorithm_view"] = open_algos_policy
527
533
 
@@ -610,4 +616,10 @@ def select_configuration_questionaire(type_: InstanceType, system_folders: bool)
610
616
  raise Exception("No configurations could be found!")
611
617
 
612
618
  # pop the question
613
- return q.select("Select the configuration you want to use:", choices=choices).ask()
619
+ try:
620
+ return q.select(
621
+ "Select the configuration you want to use:", choices=choices
622
+ ).unsafe_ask()
623
+ except KeyboardInterrupt:
624
+ error("Aborted by user!")
625
+ exit(1)
@@ -1,11 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import hashlib
3
4
  import os.path
4
5
 
5
6
  from pathlib import Path
6
7
 
7
8
  from vantage6.common.context import AppContext
8
- from vantage6.common.globals import APPNAME, InstanceType
9
+ from vantage6.common.globals import APPNAME, STRING_ENCODING, InstanceType
9
10
  from vantage6.cli.configuration_manager import NodeConfigurationManager
10
11
  from vantage6.cli.globals import DEFAULT_NODE_SYSTEM_FOLDERS as N_FOL
11
12
  from vantage6.cli._version import __version__
@@ -39,6 +40,7 @@ class NodeContext(AppContext):
39
40
  system_folders: bool = N_FOL,
40
41
  config_file: str = None,
41
42
  print_log_header: bool = True,
43
+ logger_prefix: str = "",
42
44
  ):
43
45
  super().__init__(
44
46
  InstanceType.NODE,
@@ -46,9 +48,11 @@ class NodeContext(AppContext):
46
48
  system_folders,
47
49
  config_file,
48
50
  print_log_header,
51
+ logger_prefix,
49
52
  )
50
53
  if print_log_header:
51
54
  self.log.info("vantage6 version '%s'", __version__)
55
+ self.identifier = self.__create_node_identifier()
52
56
 
53
57
  @classmethod
54
58
  def from_external_config_file(
@@ -245,3 +249,16 @@ class NodeContext(AppContext):
245
249
  URI to the database
246
250
  """
247
251
  return self.config["databases"][label]
252
+
253
+ def __create_node_identifier(self) -> str:
254
+ """
255
+ Create a unique identifier for the node.
256
+
257
+ Returns
258
+ -------
259
+ str
260
+ Unique identifier for the node
261
+ """
262
+ return hashlib.sha256(
263
+ self.config.get("api_key").encode(STRING_ENCODING)
264
+ ).hexdigest()[:16]
@@ -1,70 +1,13 @@
1
- import time
2
- from threading import Thread
3
1
  import click
4
- import questionary as q
5
- import docker
6
2
 
7
- from colorama import Fore, Style
8
-
9
- from vantage6.common import warning, error, info
10
- from vantage6.common.globals import APPNAME
11
- from vantage6.common.docker.addons import check_docker_running
12
-
13
- from vantage6.cli.common.utils import print_log_worker
14
- from vantage6.cli.globals import DEFAULT_NODE_SYSTEM_FOLDERS as N_FOL
15
- from vantage6.cli.node.common import find_running_node_names
3
+ from vantage6.common import info
4
+ from vantage6.cli.common.utils import attach_logs
16
5
 
17
6
 
18
7
  @click.command()
19
- @click.option("-n", "--name", default=None, help="Configuration name")
20
- @click.option(
21
- "--system",
22
- "system_folders",
23
- flag_value=True,
24
- help="Search for configuration in system folders rather than " "user folders",
25
- )
26
- @click.option(
27
- "--user",
28
- "system_folders",
29
- flag_value=False,
30
- default=N_FOL,
31
- help="Search for configuration in user folders rather than "
32
- "system folders. This is the default",
33
- )
34
- def cli_node_attach(name: str, system_folders: bool) -> None:
8
+ def cli_node_attach() -> None:
35
9
  """
36
10
  Show the node logs in the current console.
37
11
  """
38
- check_docker_running()
39
- client = docker.from_env()
40
-
41
- running_node_names = find_running_node_names(client)
42
-
43
- if not running_node_names:
44
- warning("No nodes are currently running. Cannot show any logs!")
45
- return
46
-
47
- if not name:
48
- name = q.select(
49
- "Select the node you wish to inspect:", choices=running_node_names
50
- ).ask()
51
- else:
52
- post_fix = "system" if system_folders else "user"
53
- name = f"{APPNAME}-{name}-{post_fix}"
54
-
55
- if name in running_node_names:
56
- container = client.containers.get(name)
57
- logs = container.attach(stream=True, logs=True)
58
- Thread(target=print_log_worker, args=(logs,), daemon=True).start()
59
- while True:
60
- try:
61
- time.sleep(1)
62
- except KeyboardInterrupt:
63
- info("Closing log file. Keyboard Interrupt.")
64
- info(
65
- "Note that your node is still running! Shut it down with "
66
- f"'{Fore.RED}v6 node stop{Style.RESET_ALL}'"
67
- )
68
- exit(0)
69
- else:
70
- error(f"{Fore.RED}{name}{Style.RESET_ALL} was not running!?")
12
+ info("Attaching to node logs...")
13
+ attach_logs("app=node")
@@ -26,8 +26,12 @@ def cli_node_clean() -> None:
26
26
  msg += volume.name + ","
27
27
  info(msg)
28
28
 
29
- confirm = q.confirm("Are you sure?")
30
- if confirm.ask():
29
+ try:
30
+ confirm = q.confirm("Are you sure?").unsafe_ask()
31
+ except KeyboardInterrupt:
32
+ confirm = False
33
+
34
+ if confirm:
31
35
  for volume in candidates:
32
36
  try:
33
37
  volume.remove()
@@ -38,7 +38,9 @@ def create_client(ctx: NodeContext) -> UserClient:
38
38
  return UserClient(host, port, api_path, log_level="warn")
39
39
 
40
40
 
41
- def create_client_and_authenticate(ctx: NodeContext) -> UserClient:
41
+ def create_client_and_authenticate(
42
+ ctx: NodeContext, ask_mfa: bool = False
43
+ ) -> UserClient:
42
44
  """
43
45
  Generate a client and authenticate with the server.
44
46
 
@@ -46,6 +48,8 @@ def create_client_and_authenticate(ctx: NodeContext) -> UserClient:
46
48
  ----------
47
49
  ctx : NodeContext
48
50
  Context of the node loaded from the configuration file
51
+ ask_mfa : bool, optional
52
+ Whether to ask for MFA code, by default False
49
53
 
50
54
  Returns
51
55
  -------
@@ -54,11 +58,14 @@ def create_client_and_authenticate(ctx: NodeContext) -> UserClient:
54
58
  """
55
59
  client = create_client(ctx)
56
60
 
57
- username = q.text("Username:").ask()
58
- password = q.password("Password:").ask()
61
+ try:
62
+ username, password, mfa_code = _get_auth_data()
63
+ except KeyboardInterrupt:
64
+ error("Authentication aborted.")
65
+ exit(1)
59
66
 
60
67
  try:
61
- client.authenticate(username, password)
68
+ client.authenticate(username, password, mfa_code=mfa_code)
62
69
 
63
70
  except Exception as exc:
64
71
  error("Could not authenticate with server!")
@@ -68,6 +75,21 @@ def create_client_and_authenticate(ctx: NodeContext) -> UserClient:
68
75
  return client
69
76
 
70
77
 
78
+ def _get_auth_data() -> tuple[str, str, str]:
79
+ """
80
+ Get authentication data from the user.
81
+
82
+ Returns
83
+ -------
84
+ tuple[str, str, str]
85
+ Tuple containing username, password and MFA code
86
+ """
87
+ username = q.text("Username:").unsafe_ask()
88
+ password = q.password("Password:").unsafe_ask()
89
+ mfa_code = q.text("MFA code:").unsafe_ask()
90
+ return username, password, mfa_code
91
+
92
+
71
93
  def select_node(name: str, system_folders: bool) -> tuple[str, str]:
72
94
  """
73
95
  Let user select node through questionnaire if name is not given.
@@ -59,6 +59,13 @@ from vantage6.cli.node.common import select_node, create_client_and_authenticate
59
59
  default=False,
60
60
  help="Overwrite existing private key if present",
61
61
  )
62
+ @click.option(
63
+ "--mfa",
64
+ "ask_mfa",
65
+ flag_value=True,
66
+ default=False,
67
+ help="Ask for multi-factor authentication code. Use this if MFA is enabled on the server.",
68
+ )
62
69
  def cli_node_create_private_key(
63
70
  name: str,
64
71
  config: str,
@@ -66,6 +73,7 @@ def cli_node_create_private_key(
66
73
  upload: bool,
67
74
  organization_name: str,
68
75
  overwrite: bool,
76
+ ask_mfa: bool,
69
77
  ) -> None:
70
78
  """
71
79
  Create and upload a new private key
@@ -89,7 +97,7 @@ def cli_node_create_private_key(
89
97
  # Authenticate with the server to obtain organization name if it wasn't
90
98
  # provided
91
99
  if organization_name is None:
92
- client = create_client_and_authenticate(ctx)
100
+ client = create_client_and_authenticate(ctx, ask_mfa)
93
101
  organization_name = client.whoami.organization_name
94
102
 
95
103
  # create directory where private key goes if it doesn't exist yet
@@ -103,7 +111,7 @@ def cli_node_create_private_key(
103
111
  warning(f"File '{Fore.CYAN}{file_}{Style.RESET_ALL}' exists!")
104
112
 
105
113
  if overwrite:
106
- warning("'--override' specified, so it will be overwritten ...")
114
+ warning("'--overwrite' specified, so it will be overwritten ...")
107
115
 
108
116
  if file_.exists() and not overwrite:
109
117
  error("Could not create private key!")
vantage6/cli/node/new.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import click
2
2
  from colorama import Fore, Style
3
3
 
4
- from vantage6.common import error, info, check_config_writeable
4
+ from vantage6.common import error, info, ensure_config_dir_writable
5
5
  from vantage6.cli.context.node import NodeContext
6
6
  from vantage6.cli.globals import DEFAULT_NODE_SYSTEM_FOLDERS as N_FOL
7
7
  from vantage6.cli.configuration_wizard import configuration_wizard
@@ -41,13 +41,17 @@ def cli_node_new_configuration(name: str, system_folders: bool) -> None:
41
41
  exit(1)
42
42
 
43
43
  # Check that we can write in this folder
44
- if not check_config_writeable(system_folders):
44
+ if not ensure_config_dir_writable(system_folders):
45
45
  error("Cannot write configuration file. Exiting...")
46
46
  exit(1)
47
47
 
48
48
  # create config in ctx location
49
49
  flag = "--system" if system_folders else ""
50
- cfg_file = configuration_wizard(InstanceType.NODE, name, system_folders)
50
+ try:
51
+ cfg_file = configuration_wizard(InstanceType.NODE, name, system_folders)
52
+ except KeyboardInterrupt:
53
+ error("Configuration creation aborted.")
54
+ exit(1)
51
55
  info(f"New configuration created: {Fore.GREEN}{cfg_file}{Style.RESET_ALL}")
52
56
  info(
53
57
  f"You can start the node by running "