vantage6 4.0.3__py3-none-any.whl → 4.1.0b0__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 (39) hide show
  1. tests_cli/test_node_cli.py +60 -55
  2. tests_cli/test_server_cli.py +30 -30
  3. vantage6/cli/_version.py +1 -1
  4. vantage6/cli/cli.py +102 -0
  5. vantage6/cli/{dev.py → dev/create.py} +18 -151
  6. vantage6/cli/dev/remove.py +63 -0
  7. vantage6/cli/dev/start.py +52 -0
  8. vantage6/cli/dev/stop.py +30 -0
  9. vantage6/cli/node/attach.py +58 -0
  10. vantage6/cli/node/clean.py +40 -0
  11. vantage6/cli/node/common/__init__.py +124 -0
  12. vantage6/cli/node/create_private_key.py +139 -0
  13. vantage6/cli/node/files.py +34 -0
  14. vantage6/cli/node/list.py +62 -0
  15. vantage6/cli/node/new.py +46 -0
  16. vantage6/cli/node/remove.py +103 -0
  17. vantage6/cli/node/set_api_key.py +45 -0
  18. vantage6/cli/node/start.py +311 -0
  19. vantage6/cli/node/stop.py +73 -0
  20. vantage6/cli/node/version.py +47 -0
  21. vantage6/cli/server/attach.py +54 -0
  22. vantage6/cli/server/common/__init__.py +146 -0
  23. vantage6/cli/server/files.py +16 -0
  24. vantage6/cli/server/import_.py +144 -0
  25. vantage6/cli/server/list.py +60 -0
  26. vantage6/cli/server/new.py +50 -0
  27. vantage6/cli/server/shell.py +42 -0
  28. vantage6/cli/server/start.py +302 -0
  29. vantage6/cli/server/stop.py +158 -0
  30. vantage6/cli/server/version.py +46 -0
  31. {vantage6-4.0.3.dist-info → vantage6-4.1.0b0.dist-info}/METADATA +7 -6
  32. vantage6-4.1.0b0.dist-info/RECORD +52 -0
  33. vantage6-4.1.0b0.dist-info/entry_points.txt +5 -0
  34. vantage6/cli/node.py +0 -1097
  35. vantage6/cli/server.py +0 -1033
  36. vantage6-4.0.3.dist-info/RECORD +0 -28
  37. vantage6-4.0.3.dist-info/entry_points.txt +0 -4
  38. {vantage6-4.0.3.dist-info → vantage6-4.1.0b0.dist-info}/WHEEL +0 -0
  39. {vantage6-4.0.3.dist-info → vantage6-4.1.0b0.dist-info}/top_level.txt +0 -0
vantage6/cli/server.py DELETED
@@ -1,1033 +0,0 @@
1
- import click
2
- import questionary as q
3
- import docker
4
- import os
5
- import time
6
- import subprocess
7
- import itertools
8
-
9
- from typing import Iterable
10
- from threading import Thread
11
- from functools import wraps
12
- from colorama import (Fore, Style)
13
- from sqlalchemy.engine.url import make_url
14
- from docker.client import DockerClient
15
-
16
- from vantage6.common import (info, warning, error, debug as debug_msg,
17
- check_config_writeable)
18
- from vantage6.common.docker.addons import (
19
- pull_if_newer, check_docker_running, remove_container,
20
- get_server_config_name, get_container, get_num_nonempty_networks,
21
- get_network, delete_network, remove_container_if_exists
22
- )
23
- from vantage6.common.docker.network_manager import NetworkManager
24
- from vantage6.common.globals import (
25
- APPNAME,
26
- STRING_ENCODING,
27
- DEFAULT_DOCKER_REGISTRY,
28
- DEFAULT_SERVER_IMAGE,
29
- DEFAULT_UI_IMAGE
30
- )
31
- from vantage6.cli.rabbitmq import split_rabbitmq_uri
32
-
33
- from vantage6.cli.globals import (
34
- DEFAULT_SERVER_SYSTEM_FOLDERS, DEFAULT_UI_PORT
35
- )
36
- from vantage6.cli.context import ServerContext
37
- from vantage6.cli.configuration_wizard import (
38
- select_configuration_questionaire,
39
- configuration_wizard
40
- )
41
- from vantage6.cli.utils import (
42
- check_config_name_allowed,
43
- prompt_config_name,
44
- remove_file
45
- )
46
- from vantage6.cli.rabbitmq.queue_manager import RabbitMQManager
47
- from vantage6.cli import __version__
48
-
49
-
50
- def click_insert_context(func: callable) -> callable:
51
- """
52
- Supply the Click function with additional context parameters. The context
53
- is then passed to the function as the first argument.
54
-
55
- Parameters
56
- ----------
57
- func : Callable
58
- function you want the context to be passed to
59
-
60
- Returns
61
- -------
62
- Callable
63
- Click function with context
64
- """
65
- @click.option('-n', '--name', default=None,
66
- help="Name of the configuration you want to use.")
67
- @click.option('-c', '--config', default=None,
68
- help='Absolute path to configuration-file; overrides NAME')
69
- @click.option('--system', 'system_folders', flag_value=True,
70
- help='Use system folders instead of user folders. This is '
71
- 'the default')
72
- @click.option('--user', 'system_folders', flag_value=False,
73
- default=DEFAULT_SERVER_SYSTEM_FOLDERS,
74
- help='Use user folders instead of system folders')
75
- @wraps(func)
76
- def func_with_context(name: str, config: str, system_folders: bool, *args,
77
- **kwargs) -> callable:
78
- """
79
- Decorator function that adds the context to the function.
80
-
81
- Returns
82
- -------
83
- Callable
84
- Decorated function
85
- """
86
- # path to configuration file always overrides name
87
- if config:
88
- ctx = ServerContext.from_external_config_file(
89
- config,
90
- system_folders
91
- )
92
- return func(ctx, *args, **kwargs)
93
-
94
- # in case no name is supplied, ask the user to select one
95
- if not name:
96
- try:
97
- # select configuration if none supplied
98
- name = select_configuration_questionaire(
99
- "server", system_folders
100
- )
101
- except Exception:
102
- error("No configurations could be found!")
103
- exit(1)
104
-
105
- ctx = get_server_context(name, system_folders)
106
- return func(ctx, *args, **kwargs)
107
-
108
- return func_with_context
109
-
110
-
111
- @click.group(name='server')
112
- def cli_server() -> None:
113
- """
114
- The `vserver` commands allow you to manage your vantage6 server instances.
115
- """
116
-
117
-
118
- #
119
- # start
120
- #
121
- @cli_server.command(name='start')
122
- @click.option('--ip', default=None, help='IP address to listen on')
123
- @click.option('-p', '--port', default=None, type=int, help='Port to listen on')
124
- @click.option('-i', '--image', default=None, help="Server Docker image to use")
125
- @click.option('--with-ui', 'start_ui', flag_value=True, default=False,
126
- help="Start the graphical User Interface as well")
127
- @click.option('--ui-port', default=None, type=int,
128
- help="Port to listen on for the User Interface")
129
- @click.option('--with-rabbitmq', 'start_rabbitmq', flag_value=True,
130
- default=False, help="Start RabbitMQ message broker as local "
131
- "container - use in development only")
132
- @click.option('--rabbitmq-image', default=None,
133
- help="RabbitMQ docker image to use")
134
- @click.option('--keep/--auto-remove', default=False,
135
- help="Keep image after server has stopped. Useful for debugging")
136
- @click.option('--mount-src', default='',
137
- help="Override vantage6 source code in container with the source"
138
- " code in this path")
139
- @click.option('--attach/--detach', default=False,
140
- help="Print server logs to the console after start")
141
- @click_insert_context
142
- def cli_server_start(ctx: ServerContext, ip: str, port: int, image: str,
143
- start_ui: bool, ui_port: int, start_rabbitmq: bool,
144
- rabbitmq_image: str, keep: bool, mount_src: str,
145
- attach: bool) -> None:
146
- """
147
- Start the server.
148
- """
149
- vserver_start(ctx, ip, port, image, start_ui, ui_port, start_rabbitmq,
150
- rabbitmq_image, keep, mount_src, attach)
151
-
152
-
153
- def vserver_start(ctx: ServerContext, ip: str, port: int, image: str,
154
- start_ui: bool, ui_port: int, start_rabbitmq: bool,
155
- rabbitmq_image: str, keep: bool, mount_src: str,
156
- attach: bool) -> None:
157
- """
158
- Start the server in a Docker container.
159
-
160
- Parameters
161
- ----------
162
- ctx : ServerContext
163
- Server context object
164
- ip : str
165
- ip interface to listen on
166
- port : int
167
- port to listen on
168
- image : str
169
- Server Docker image to use
170
- start_ui : bool
171
- Start the graphical User Interface as well
172
- ui_port : int
173
- Port to listen on for the User Interface
174
- start_rabbitmq : bool
175
- Start RabbitMQ message broker as local container - use only in
176
- development
177
- rabbitmq_image : str
178
- RabbitMQ docker image to use
179
- keep : bool
180
- Wether to keep the image after the server has finished, useful for
181
- debugging
182
- mount_src : str
183
- Path to the vantage6 package source, this overrides the source code in
184
- the container. This is useful when developing and testing the server.
185
- attach : bool
186
- Wether to attach the server logs to the console after starting the
187
- server.
188
- """
189
- # will print an error if not
190
- check_docker_running()
191
-
192
- info("Starting server...")
193
- info("Finding Docker daemon.")
194
- docker_client = docker.from_env()
195
-
196
- # check if name is allowed for docker volume, else exit
197
- check_config_name_allowed(ctx.name)
198
-
199
- # check that this server is not already running
200
- running_servers = docker_client.containers.list(
201
- filters={"label": f"{APPNAME}-type=server"})
202
- for server in running_servers:
203
- if server.name == f"{APPNAME}-{ctx.name}-{ctx.scope}-server":
204
- error(f"Server {Fore.RED}{ctx.name}{Style.RESET_ALL} "
205
- "is already running")
206
- exit(1)
207
-
208
- # Determine image-name. First we check if the option --image has been used.
209
- # Then we check if the image has been specified in the config file, and
210
- # finally we use the default settings from the package.
211
- if image is None:
212
- custom_images: dict = ctx.config.get('images')
213
- if custom_images:
214
- image = custom_images.get('server')
215
- if not image:
216
- image = f"{DEFAULT_DOCKER_REGISTRY}/{DEFAULT_SERVER_IMAGE}"
217
-
218
- info(f"Pulling latest server image '{image}'.")
219
- try:
220
- pull_if_newer(docker.from_env(), image)
221
- # docker_client.images.pull(image)
222
- except Exception as e:
223
- warning(' ... Getting latest server image failed:')
224
- warning(f" {e}")
225
- else:
226
- info(" ... success!")
227
-
228
- info("Creating mounts")
229
- config_file = "/mnt/config.yaml"
230
- mounts = [
231
- docker.types.Mount(
232
- config_file, str(ctx.config_file), type="bind"
233
- )
234
- ]
235
-
236
- if mount_src:
237
- mount_src = os.path.abspath(mount_src)
238
- mounts.append(docker.types.Mount("/vantage6", mount_src, type="bind"))
239
- # FIXME: code duplication with cli_server_import()
240
- # try to mount database
241
- uri = ctx.config['uri']
242
- url = make_url(uri)
243
- environment_vars = None
244
-
245
- # If host is None, we're dealing with a file-based DB, like SQLite
246
- if (url.host is None):
247
- db_path = url.database
248
-
249
- if not os.path.isabs(db_path):
250
- # We're dealing with a relative path here -> make it absolute
251
- db_path = ctx.data_dir / url.database
252
-
253
- basename = os.path.basename(db_path)
254
- dirname = os.path.dirname(db_path)
255
- os.makedirs(dirname, exist_ok=True)
256
-
257
- # we're mounting the entire folder that contains the database
258
- mounts.append(docker.types.Mount(
259
- "/mnt/database/", dirname, type="bind"
260
- ))
261
-
262
- environment_vars = {
263
- "VANTAGE6_DB_URI": f"sqlite:////mnt/database/{basename}",
264
- "VANTAGE6_CONFIG_NAME": ctx.config_file_name
265
- }
266
-
267
- else:
268
- warning(f"Database could not be transferred, make sure {url.host} "
269
- "is reachable from the Docker container")
270
- info("Consider using the docker-compose method to start a server")
271
-
272
- # Create a docker network for the server and other services like RabbitMQ
273
- # to reside in
274
- server_network_mgr = NetworkManager(
275
- network_name=f"{APPNAME}-{ctx.name}-{ctx.scope}-network"
276
- )
277
- server_network_mgr.create_network(is_internal=False)
278
-
279
- if start_rabbitmq or ctx.config.get('rabbitmq') and \
280
- ctx.config['rabbitmq'].get('start_with_server', False):
281
- # Note that ctx.data_dir has been created at this point, which is
282
- # required for putting some RabbitMQ configuration files inside
283
- info('Starting RabbitMQ container')
284
- _start_rabbitmq(ctx, rabbitmq_image, server_network_mgr)
285
- elif ctx.config.get('rabbitmq'):
286
- info("RabbitMQ is provided in the config file as external service. "
287
- "Assuming this service is up and running.")
288
- else:
289
- warning('Message queue disabled! This means that the vantage6 server '
290
- 'cannot be scaled horizontally!')
291
-
292
- # start the UI if requested
293
- if start_ui or ctx.config.get('ui') and ctx.config['ui'].get('enabled'):
294
- _start_ui(docker_client, ctx, ui_port)
295
-
296
- # The `ip` and `port` refer here to the ip and port within the container.
297
- # So we do not really care that is it listening on all interfaces.
298
- internal_port = 5000
299
- cmd = (
300
- f'uwsgi --http :{internal_port} --gevent 1000 --http-websockets '
301
- '--master --callable app --disable-logging '
302
- '--wsgi-file /vantage6/vantage6-server/vantage6/server/wsgi.py '
303
- f'--pyargv {config_file}'
304
- )
305
- info(cmd)
306
-
307
- info("Run Docker container")
308
- port_ = str(port or ctx.config["port"] or 5000)
309
- container = docker_client.containers.run(
310
- image,
311
- command=cmd,
312
- mounts=mounts,
313
- detach=True,
314
- labels={
315
- f"{APPNAME}-type": "server",
316
- "name": ctx.config_file_name
317
- },
318
- environment=environment_vars,
319
- ports={f"{internal_port}/tcp": (ip, port_)},
320
- name=ctx.docker_container_name,
321
- auto_remove=not keep,
322
- tty=True,
323
- network=server_network_mgr.network_name
324
- )
325
-
326
- info(f"Success! container id = {container.id}")
327
-
328
- if attach:
329
- logs = container.attach(stream=True, logs=True, stdout=True)
330
- Thread(target=_print_log_worker, args=(logs,), daemon=True).start()
331
- while True:
332
- try:
333
- time.sleep(1)
334
- except KeyboardInterrupt:
335
- info("Closing log file. Keyboard Interrupt.")
336
- info("Note that your server is still running! Shut it down "
337
- f"with {Fore.RED}vserver stop{Style.RESET_ALL}")
338
- exit(0)
339
-
340
-
341
- #
342
- # list
343
- #
344
- @cli_server.command(name='list')
345
- def cli_server_configuration_list() -> None:
346
- """
347
- Print the available server configurations.
348
- """
349
- check_docker_running()
350
- client = docker.from_env()
351
-
352
- running_server = client.containers.list(
353
- filters={"label": f"{APPNAME}-type=server"})
354
- running_node_names = []
355
- for node in running_server:
356
- running_node_names.append(node.name)
357
-
358
- header = \
359
- "\nName"+(21*" ") + \
360
- "Status"+(10*" ") + \
361
- "System/User"
362
-
363
- click.echo(header)
364
- click.echo("-"*len(header))
365
-
366
- running = Fore.GREEN + "Running" + Style.RESET_ALL
367
- stopped = Fore.RED + "Not running" + Style.RESET_ALL
368
-
369
- # system folders
370
- configs, f1 = ServerContext.available_configurations(system_folders=True)
371
- for config in configs:
372
- status = running if f"{APPNAME}-{config.name}-system-server" in \
373
- running_node_names else stopped
374
- click.echo(
375
- f"{config.name:25}"
376
- f"{status:25} System "
377
- )
378
-
379
- # user folders
380
- configs, f2 = ServerContext.available_configurations(system_folders=False)
381
- for config in configs:
382
- status = running if f"{APPNAME}-{config.name}-user-server" in \
383
- running_node_names else stopped
384
- click.echo(
385
- f"{config.name:25}"
386
- f"{status:25} User "
387
- )
388
-
389
- click.echo("-"*85)
390
- if len(f1)+len(f2):
391
- warning(
392
- f"{Fore.RED}Failed imports: {len(f1)+len(f2)}{Style.RESET_ALL}")
393
-
394
-
395
- #
396
- # files
397
- #
398
- @cli_server.command(name='files')
399
- @click_insert_context
400
- def cli_server_files(ctx: ServerContext) -> None:
401
- """
402
- List files that belong to a particular server instance.
403
- """
404
- info(f"Configuration file = {ctx.config_file}")
405
- info(f"Log file = {ctx.log_file}")
406
- info(f"Database = {ctx.get_database_uri()}")
407
-
408
-
409
- #
410
- # new
411
- #
412
- @cli_server.command(name='new')
413
- @click.option('-n', '--name', default=None,
414
- help="name of the configuration you want to use.")
415
- @click.option('--system', 'system_folders', flag_value=True)
416
- @click.option('--user', 'system_folders', flag_value=False,
417
- default=DEFAULT_SERVER_SYSTEM_FOLDERS)
418
- def cli_server_new(name: str, system_folders: bool) -> None:
419
- """
420
- Create a new server configuration.
421
- """
422
- name = prompt_config_name(name)
423
-
424
- # check if name is allowed for docker volume, else exit
425
- check_config_name_allowed(name)
426
-
427
- # check that this config does not exist
428
- try:
429
- if ServerContext.config_exists(name, system_folders):
430
- error(f"Configuration {Fore.RED}{name}{Style.RESET_ALL} already "
431
- "exists!")
432
- exit(1)
433
- except Exception as e:
434
- error(e)
435
- exit(1)
436
-
437
- # Check that we can write in this folder
438
- if not check_config_writeable(system_folders):
439
- error("Your user does not have write access to all folders. Exiting")
440
- info(f"Create a new server using '{Fore.GREEN}vserver new "
441
- f"--user{Style.RESET_ALL}' instead!")
442
- exit(1)
443
-
444
- # create config in ctx location
445
- cfg_file = configuration_wizard("server", name, system_folders)
446
- info(f"New configuration created: {Fore.GREEN}{cfg_file}{Style.RESET_ALL}")
447
-
448
- # info(f"root user created.")
449
- flag = "" if system_folders else "--user"
450
- info(f"You can start the server by running {Fore.GREEN}vserver start "
451
- f"{flag}{Style.RESET_ALL}")
452
-
453
-
454
- #
455
- # import
456
- #
457
- # TODO this method has a lot of duplicated code from `start`
458
- @cli_server.command(name='import')
459
- @click.argument('file', type=click.Path(exists=True))
460
- @click.option('--drop-all', is_flag=True, default=False,
461
- help="Drop all existing data before importing")
462
- @click.option('-i', '--image', default=None, help="Node Docker image to use")
463
- @click.option('--mount-src', default='',
464
- help="Override vantage6 source code in container with the source"
465
- " code in this path")
466
- @click.option('--keep/--auto-remove', default=False,
467
- help="Keep image after finishing. Useful for debugging")
468
- @click.option('--wait', default=False, help="Wait for the import to finish")
469
- @click_insert_context
470
- def cli_server_import(
471
- ctx: ServerContext, file: str, drop_all: bool, image: str, mount_src: str,
472
- keep: bool, wait: bool
473
- ) -> None:
474
- """
475
- Import vantage6 resources, such as organizations, collaborations, users and
476
- tasks into a server instance.
477
-
478
- The FILE_ argument should be a path to a yaml file containing the vantage6
479
- formatted data to import.
480
- """
481
- vserver_import(ctx, file, drop_all, image, mount_src, keep,
482
- wait)
483
-
484
-
485
- def vserver_import(ctx: ServerContext, file: str, drop_all: bool,
486
- image: str, mount_src: str, keep: bool, wait: bool) -> None:
487
- """Batch import organizations/collaborations/users and tasks.
488
-
489
- Parameters
490
- ----------
491
- ctx : ServerContext
492
- Server context object
493
- file : str
494
- Yaml file containing the vantage6 formatted data to import
495
- drop_all : bool
496
- Wether to drop all data before importing
497
- image : str
498
- Node Docker image to use which contains the import script
499
- mount_src : str
500
- Vantage6 source location, this will overwrite the source code in the
501
- container. Useful for debugging/development.
502
- keep : bool
503
- Wether to keep the image after finishing/crashing. Useful for
504
- debugging.
505
- wait : bool
506
- Wether to wait for the import to finish before exiting this function
507
- """
508
- # will print an error if not
509
- check_docker_running()
510
-
511
- info("Starting server...")
512
- info("Finding Docker daemon.")
513
- docker_client = docker.from_env()
514
-
515
- # check if name is allowed for docker volume, else exit
516
- check_config_name_allowed(ctx.name)
517
-
518
- # pull latest Docker image
519
- if image is None:
520
- image = ctx.config.get(
521
- "image",
522
- f"{DEFAULT_DOCKER_REGISTRY}/{DEFAULT_SERVER_IMAGE}"
523
- )
524
- info(f"Pulling latest server image '{image}'.")
525
- try:
526
- docker_client.images.pull(image)
527
- except Exception as e:
528
- warning(' ... Getting latest node image failed:')
529
- warning(f" {e}")
530
- else:
531
- info(" ... success!")
532
-
533
- info("Creating mounts")
534
- mounts = [
535
- docker.types.Mount(
536
- "/mnt/config.yaml", str(ctx.config_file), type="bind"
537
- ),
538
- docker.types.Mount(
539
- "/mnt/import.yaml", str(file), type="bind"
540
- )
541
- ]
542
-
543
- # FIXME: code duplication with cli_server_start()
544
- # try to mount database
545
- uri = ctx.config['uri']
546
- url = make_url(uri)
547
- environment_vars = None
548
-
549
- if mount_src:
550
- mount_src = os.path.abspath(mount_src)
551
- mounts.append(docker.types.Mount("/vantage6", mount_src, type="bind"))
552
-
553
- # If host is None, we're dealing with a file-based DB, like SQLite
554
- if (url.host is None):
555
- db_path = url.database
556
-
557
- if not os.path.isabs(db_path):
558
- # We're dealing with a relative path here -> make it absolute
559
- db_path = ctx.data_dir / url.database
560
-
561
- basename = os.path.basename(db_path)
562
- dirname = os.path.dirname(db_path)
563
- os.makedirs(dirname, exist_ok=True)
564
-
565
- # we're mounting the entire folder that contains the database
566
- mounts.append(docker.types.Mount(
567
- "/mnt/database/", dirname, type="bind"
568
- ))
569
-
570
- environment_vars = {
571
- "VANTAGE6_DB_URI": f"sqlite:////mnt/database/{basename}"
572
- }
573
-
574
- else:
575
- warning(f"Database could not be transferred, make sure {url.host} "
576
- "is reachable from the Docker container")
577
- info("Consider using the docker-compose method to start a server")
578
-
579
- drop_all_ = "--drop-all" if drop_all else ""
580
- cmd = (f'vserver-local import -c /mnt/config.yaml {drop_all_} '
581
- '/mnt/import.yaml')
582
-
583
- info(cmd)
584
-
585
- info("Run Docker container")
586
- container = docker_client.containers.run(
587
- image,
588
- command=cmd,
589
- mounts=mounts,
590
- detach=True,
591
- labels={
592
- f"{APPNAME}-type": "server",
593
- "name": ctx.config_file_name
594
- },
595
- environment=environment_vars,
596
- auto_remove=not keep,
597
- tty=True
598
- )
599
- logs = container.logs(stream=True, stdout=True)
600
- Thread(target=_print_log_worker, args=(logs,), daemon=False).start()
601
-
602
- info(f"Success! container id = {container.id}")
603
-
604
- if wait:
605
- container.wait()
606
- info("Container finished!")
607
-
608
-
609
- #
610
- # shell
611
- #
612
- @cli_server.command(name='shell')
613
- @click_insert_context
614
- def cli_server_shell(ctx: ServerContext) -> None:
615
- """
616
- Run an iPython shell within a running server. This can be used to modify
617
- the database.
618
-
619
- NOTE: using the shell is no longer recommended as there is no validation on
620
- the changes that you make. It is better to use the Python client or a
621
- graphical user interface instead.
622
- """
623
- # will print an error if not
624
- check_docker_running()
625
-
626
- docker_client = docker.from_env()
627
-
628
- running_servers = docker_client.containers.list(
629
- filters={"label": f"{APPNAME}-type=server"})
630
-
631
- if ctx.docker_container_name not in [s.name for s in running_servers]:
632
- error(f"Server {Fore.RED}{ctx.name}{Style.RESET_ALL} is not running?")
633
- return
634
-
635
- try:
636
- subprocess.run(['docker', 'exec', '-it', ctx.docker_container_name,
637
- 'vserver-local', 'shell', '-c', '/mnt/config.yaml'])
638
- except Exception as e:
639
- info("Failed to start subprocess...")
640
- debug_msg(e)
641
-
642
-
643
- #
644
- # stop
645
- #
646
- @cli_server.command(name='stop')
647
- @click.option("-n", "--name", default=None, help="Configuration name")
648
- @click.option('--system', 'system_folders', flag_value=True)
649
- @click.option('--user', 'system_folders', flag_value=False,
650
- default=DEFAULT_SERVER_SYSTEM_FOLDERS)
651
- @click.option('--all', 'all_servers', flag_value=True, help="Stop all servers")
652
- def cli_server_stop(name: str, system_folders: bool, all_servers: bool):
653
- """
654
- Stop one or all running server(s).
655
- """
656
- vserver_stop(name, system_folders, all_servers)
657
-
658
-
659
- def vserver_stop(name: str, system_folders: bool, all_servers: bool) -> None:
660
- """
661
- Stop one or all running server(s).
662
-
663
- Parameters
664
- ----------
665
- name : str
666
- Name of the server to stop
667
- system_folders : bool
668
- Wether to use system folders or not
669
- all_servers : bool
670
- Wether to stop all servers or not
671
- """
672
- check_docker_running()
673
- client = docker.from_env()
674
-
675
- running_servers = client.containers.list(
676
- filters={"label": f"{APPNAME}-type=server"})
677
-
678
- if not running_servers:
679
- warning("No servers are currently running.")
680
- return
681
-
682
- running_server_names = [server.name for server in running_servers]
683
-
684
- if all_servers:
685
- for container_name in running_server_names:
686
- _stop_server_containers(client, container_name, system_folders)
687
- return
688
-
689
- # make sure we have a configuration name to work with
690
- if not name:
691
- container_name = q.select("Select the server you wish to stop:",
692
- choices=running_server_names).ask()
693
- else:
694
- post_fix = "system" if system_folders else "user"
695
- container_name = f"{APPNAME}-{name}-{post_fix}-server"
696
-
697
- if container_name not in running_server_names:
698
- error(f"{Fore.RED}{name}{Style.RESET_ALL} is not running!")
699
- return
700
-
701
- _stop_server_containers(client, container_name, system_folders)
702
-
703
-
704
- #
705
- # attach
706
- #
707
- @cli_server.command(name='attach')
708
- @click.option("-n", "--name", default=None, help="configuration name")
709
- @click.option('--system', 'system_folders', flag_value=True)
710
- @click.option('--user', 'system_folders', flag_value=False,
711
- default=DEFAULT_SERVER_SYSTEM_FOLDERS)
712
- def cli_server_attach(name: str, system_folders: bool) -> None:
713
- """
714
- Show the server logs in the current console.
715
- """
716
- check_docker_running()
717
- client = docker.from_env()
718
-
719
- running_servers = client.containers.list(
720
- filters={"label": f"{APPNAME}-type=server"})
721
- running_server_names = [node.name for node in running_servers]
722
-
723
- if not name:
724
- name = q.select("Select the server you wish to inspect:",
725
- choices=running_server_names).ask()
726
- else:
727
- post_fix = "system" if system_folders else "user"
728
- name = f"{APPNAME}-{name}-{post_fix}-server"
729
-
730
- if name in running_server_names:
731
- container = client.containers.get(name)
732
- logs = container.attach(stream=True, logs=True, stdout=True)
733
- Thread(target=_print_log_worker, args=(logs,), daemon=True).start()
734
- while True:
735
- try:
736
- time.sleep(1)
737
- except KeyboardInterrupt:
738
- info("Closing log file. Keyboard Interrupt.")
739
- info("Note that your server is still running! Shut it down "
740
- f"with {Fore.RED}vserver stop{Style.RESET_ALL}")
741
- exit(0)
742
- else:
743
- error(f"{Fore.RED}{name}{Style.RESET_ALL} was not running!?")
744
-
745
-
746
- #
747
- # version
748
- #
749
- @cli_server.command(name='version')
750
- @click.option("-n", "--name", default=None, help="Configuration name")
751
- @click.option('--system', 'system_folders', flag_value=True)
752
- @click.option('--user', 'system_folders', flag_value=False,
753
- default=DEFAULT_SERVER_SYSTEM_FOLDERS)
754
- def cli_server_version(name: str, system_folders: bool) -> None:
755
- """
756
- Print the version of the vantage6 server.
757
- """
758
- check_docker_running()
759
- client = docker.from_env()
760
-
761
- running_servers = client.containers.list(
762
- filters={"label": f"{APPNAME}-type=server"})
763
- running_server_names = [server.name for server in running_servers]
764
-
765
- if not name:
766
- if not running_server_names:
767
- error("No servers are running! You can only check the version for "
768
- "servers that are running")
769
- exit(1)
770
- name = q.select("Select the server you wish to inspect:",
771
- choices=running_server_names).ask()
772
- else:
773
- post_fix = "system" if system_folders else "user"
774
- name = f"{APPNAME}-{name}-{post_fix}"
775
-
776
- if name in running_server_names:
777
- container = client.containers.get(name)
778
- version = container.exec_run(cmd='vserver-local version',
779
- stdout=True)
780
- click.echo({"server": version.output.decode('utf-8'),
781
- "cli": __version__})
782
- else:
783
- error(f"Server {name} is not running! Cannot provide version...")
784
-
785
-
786
- #
787
- # helper functions
788
- #
789
- def get_server_context(name: str, system_folders: bool) \
790
- -> ServerContext:
791
- """
792
- Load the server context from the configuration file.
793
-
794
- Parameters
795
- ----------
796
- name : str
797
- Name of the server to inspect
798
- system_folders : bool
799
- Wether to use system folders or if False, the user folders
800
-
801
- Returns
802
- -------
803
- ServerContext
804
- Server context object
805
- """
806
- if not ServerContext.config_exists(name, system_folders):
807
- scope = "system" if system_folders else "user"
808
- error(
809
- f"Configuration {Fore.RED}{name}{Style.RESET_ALL} does not "
810
- f"exist in the {Fore.RED}{scope}{Style.RESET_ALL} folders!"
811
- )
812
- exit(1)
813
-
814
- # We do not want to log this here, we do this in the container and not on
815
- # the host. We only want CLI logging here.
816
- ServerContext.LOGGING_ENABLED = False
817
-
818
- # create server context, and initialize db
819
- ctx = ServerContext(name, system_folders=system_folders)
820
-
821
- return ctx
822
-
823
-
824
- def _start_rabbitmq(ctx: ServerContext, rabbitmq_image: str,
825
- network_mgr: NetworkManager) -> None:
826
- """
827
- Start the RabbitMQ container if it is not already running.
828
-
829
- Parameters
830
- ----------
831
- ctx : ServerContext
832
- Server context object
833
- rabbitmq_image : str
834
- RabbitMQ image to use
835
- network_mgr : NetworkManager
836
- Network manager object
837
- """
838
- rabbit_uri = ctx.config['rabbitmq'].get('uri')
839
- if not rabbit_uri:
840
- error("No RabbitMQ URI found in the configuration file! Please add"
841
- "a 'uri' key to the 'rabbitmq' section of the configuration.")
842
- exit(1)
843
- # kick off RabbitMQ container
844
- rabbit_mgr = RabbitMQManager(
845
- ctx=ctx, network_mgr=network_mgr, image=rabbitmq_image)
846
- rabbit_mgr.start()
847
-
848
-
849
- def _stop_server_containers(client: DockerClient, container_name: str,
850
- system_folders: bool) -> None:
851
- """
852
- Given a server's name, kill its docker container and related (RabbitMQ)
853
- containers.
854
-
855
- Parameters
856
- ----------
857
- client : DockerClient
858
- Docker client
859
- container_name : str
860
- Name of the server to stop
861
- system_folders : bool
862
- Wether to use system folders or not
863
- """
864
- # kill the server
865
- remove_container_if_exists(client, name=container_name)
866
- info(f"Stopped the {Fore.GREEN}{container_name}{Style.RESET_ALL} server.")
867
-
868
- # find the configuration name from the docker container name
869
- # server name is formatted as f"{APPNAME}-{self.name}-{self.scope}-server"
870
- scope = "system" if system_folders else "user"
871
- config_name = get_server_config_name(container_name, scope)
872
-
873
- ctx = get_server_context(config_name, system_folders)
874
-
875
- # kill the UI container (if it exists)
876
- _stop_ui(client, ctx)
877
-
878
- # delete the server network
879
- network_name = f"{APPNAME}-{ctx.name}-{ctx.scope}-network"
880
- network = get_network(client, name=network_name)
881
- delete_network(network, kill_containers=False)
882
-
883
- # kill RabbitMQ if it exists and no other servers are using to it (i.e. it
884
- # is not in other docker networks with other containers)
885
- rabbit_uri = ctx.config.get('rabbitmq', {}).get('uri')
886
- if rabbit_uri:
887
- rabbit_container_name = split_rabbitmq_uri(
888
- rabbit_uri=rabbit_uri)['host']
889
- rabbit_container = get_container(client, name=rabbit_container_name)
890
- if rabbit_container and \
891
- get_num_nonempty_networks(rabbit_container) == 0:
892
- remove_container(rabbit_container, kill=True)
893
- info(f"Stopped the {Fore.GREEN}{rabbit_container_name}"
894
- f"{Style.RESET_ALL} container.")
895
-
896
-
897
- def _print_log_worker(logs_stream: Iterable[bytes]) -> None:
898
- """
899
- Print the logs from the docker container to the terminal.
900
-
901
- Parameters
902
- ----------
903
- logs_stream : Iterable[bytes]
904
- Output of the `container.attach(.)` method
905
- """
906
- for log in logs_stream:
907
- print(log.decode(STRING_ENCODING), end="")
908
-
909
-
910
- def vserver_remove(ctx: ServerContext, name: str, system_folders: bool,
911
- force: bool) -> None:
912
- """
913
- Function to remove a server.
914
-
915
- Parameters
916
- ----------
917
- ctx : ServerContext
918
- Server context object
919
- name : str
920
- Name of the server to remove
921
- system_folders : bool
922
- Whether to use system folders or not
923
- force : bool
924
- Whether to ask for confirmation before removing or not
925
- """
926
- check_docker_running()
927
-
928
- # first stop server
929
- vserver_stop(name, system_folders, False)
930
-
931
- if not force:
932
- if not q.confirm(
933
- "This server will be deleted permanently including its "
934
- "configuration. Are you sure?", default=False
935
- ).ask():
936
- info("Server will not be deleted")
937
- exit(0)
938
-
939
- # now remove the folders...
940
- info(f"Removing configuration file {ctx.config_file}")
941
- remove_file(ctx.config_file, 'configuration')
942
-
943
- info(f"Removing log file {ctx.log_file}")
944
- for handler in itertools.chain(ctx.log.handlers, ctx.log.root.handlers):
945
- handler.close()
946
- remove_file(ctx.log_file, 'log')
947
-
948
-
949
- def _start_ui(client: DockerClient, ctx: ServerContext, ui_port: int) -> None:
950
- """
951
- Start the UI container.
952
-
953
- Parameters
954
- ----------
955
- client : DockerClient
956
- Docker client
957
- ctx : ServerContext
958
- Server context object
959
- ui_port : int
960
- Port to expose the UI on
961
- """
962
- # if no port is specified, check if config contains a port
963
- ui_config = ctx.config.get('ui')
964
- if ui_config and not ui_port:
965
- ui_port = ui_config.get('port')
966
-
967
- # check if the port is valid
968
- # TODO make function to check if port is valid, and use in more places
969
- if not isinstance(ui_port, int) or not 0 < ui_port < 65536:
970
- warning(f"UI port '{ui_port}' is not valid! Using default port "
971
- f"{DEFAULT_UI_PORT}")
972
- ui_port = DEFAULT_UI_PORT
973
-
974
- # find image to use
975
- custom_images: dict = ctx.config.get('images')
976
- image = None
977
- if custom_images:
978
- image = custom_images.get('ui')
979
- if not image:
980
- image = f"{DEFAULT_DOCKER_REGISTRY}/{DEFAULT_UI_IMAGE}"
981
-
982
- info(f"Pulling latest UI image '{image}'.")
983
- try:
984
- pull_if_newer(docker.from_env(), image)
985
- # docker_client.images.pull(image)
986
- except Exception as e:
987
- warning(' ... Getting latest node image failed:')
988
- warning(f" {e}")
989
- else:
990
- info(" ... success!")
991
-
992
- # set environment variables
993
- env_vars = {
994
- "SERVER_URL": f"http://localhost:{ctx.config.get('port')}",
995
- "API_PATH": ctx.config.get("api_path"),
996
- }
997
-
998
- # stop the UI container if it is already running
999
- _stop_ui(client, ctx)
1000
-
1001
- info(f'Starting User Interface at port {ui_port}')
1002
- ui_container_name = f"{APPNAME}-{ctx.name}-{ctx.scope}-ui"
1003
- client.containers.run(
1004
- image,
1005
- detach=True,
1006
- labels={
1007
- f"{APPNAME}-type": "ui",
1008
- "name": ctx.config_file_name
1009
- },
1010
- ports={"80/tcp": (ctx.config.get('ip'), ui_port)},
1011
- name=ui_container_name,
1012
- environment=env_vars,
1013
- tty=True,
1014
- )
1015
-
1016
-
1017
- def _stop_ui(client: DockerClient, ctx: ServerContext) -> None:
1018
- """
1019
- Check if the UI container is running, and if so, stop and remove it.
1020
-
1021
- Parameters
1022
- ----------
1023
- client : DockerClient
1024
- Docker client
1025
- ctx : ServerContext
1026
- Server context object
1027
- """
1028
- ui_container_name = f"{APPNAME}-{ctx.name}-{ctx.scope}-ui"
1029
- ui_container = get_container(client, name=ui_container_name)
1030
- if ui_container:
1031
- remove_container(ui_container, kill=True)
1032
- info(f"Stopped the {Fore.GREEN}{ui_container_name}"
1033
- f"{Style.RESET_ALL} User Interface container.")