From adce386a5506a20af6e46d3f7d801de48faf4593 Mon Sep 17 00:00:00 2001 From: Pallab Pain Date: Thu, 17 Oct 2024 17:38:46 +0530 Subject: [PATCH] chore: lint and format code --- CONTRIBUTING.md | 7 + docs/source/conf.py | 47 +- docs/source/rapyuta/__init__.py | 3 +- riocli/__init__.py | 3 +- riocli/apply/__init__.py | 234 ++++++--- riocli/apply/explain.py | 33 +- riocli/apply/filters.py | 2 +- riocli/apply/parse.py | 239 +++++---- riocli/apply/template.py | 37 +- riocli/apply/util.py | 50 +- riocli/auth/login.py | 97 ++-- riocli/auth/logout.py | 18 +- riocli/auth/refresh_token.py | 36 +- riocli/auth/staging.py | 78 +-- riocli/auth/status.py | 8 +- riocli/auth/token.py | 20 +- riocli/auth/util.py | 121 ++--- riocli/bootstrap.py | 46 +- riocli/chart/apply.py | 111 +++-- riocli/chart/chart.py | 75 +-- riocli/chart/delete.py | 71 ++- riocli/chart/info.py | 6 +- riocli/chart/list.py | 11 +- riocli/chart/search.py | 12 +- riocli/chart/util.py | 32 +- riocli/completion/__init__.py | 10 +- riocli/config/config.py | 88 ++-- riocli/config/context.py | 32 +- riocli/configtree/__init__.py | 15 +- riocli/configtree/diff.py | 19 +- riocli/configtree/etcd.py | 85 ++-- riocli/configtree/export_keys.py | 70 ++- riocli/configtree/import_keys.py | 226 ++++++--- riocli/configtree/merge.py | 136 ++++-- riocli/configtree/revision.py | 431 ++++++++++------- riocli/configtree/tree.py | 220 +++++---- riocli/configtree/util.py | 157 +++--- riocli/constants/colors.py | 33 +- riocli/constants/regions.py | 5 +- riocli/constants/status.py | 12 +- riocli/constants/symbols.py | 11 +- riocli/deployment/delete.py | 78 +-- riocli/deployment/execute.py | 51 +- riocli/deployment/inspect.py | 23 +- riocli/deployment/list.py | 103 ++-- riocli/deployment/logs.py | 20 +- riocli/deployment/model.py | 32 +- riocli/deployment/status.py | 4 +- riocli/deployment/update.py | 130 ++--- riocli/deployment/util.py | 34 +- riocli/deployment/wait.py | 15 +- riocli/device/__init__.py | 4 +- riocli/device/config.py | 90 ++-- riocli/device/create.py | 77 ++- riocli/device/delete.py | 95 ++-- riocli/device/deployment.py | 16 +- riocli/device/execute.py | 16 +- riocli/device/files.py | 165 ++++--- riocli/device/inspect.py | 15 +- riocli/device/label.py | 42 +- riocli/device/list.py | 4 +- riocli/device/migrate.py | 67 ++- riocli/device/model.py | 56 ++- riocli/device/onboard.py | 6 +- riocli/device/tools/__init__.py | 4 +- riocli/device/tools/device_init.py | 57 ++- riocli/device/tools/forward.py | 14 +- riocli/device/tools/rapyuta_logs.py | 11 +- riocli/device/tools/scp.py | 18 +- riocli/device/tools/service.py | 64 ++- riocli/device/tools/ssh.py | 100 ++-- riocli/device/tools/util.py | 36 +- riocli/device/topic.py | 48 +- riocli/device/util.py | 113 ++--- riocli/device/vpn.py | 85 ++-- riocli/disk/create.py | 43 +- riocli/disk/delete.py | 74 +-- riocli/disk/enum.py | 2 +- riocli/disk/list.py | 15 +- riocli/disk/model.py | 4 +- riocli/disk/util.py | 32 +- riocli/exceptions/__init__.py | 15 +- riocli/hwil/create.py | 66 ++- riocli/hwil/delete.py | 44 +- riocli/hwil/execute.py | 6 +- riocli/hwil/inspect.py | 19 +- riocli/hwil/list.py | 8 +- riocli/hwil/login.py | 56 ++- riocli/hwil/ssh.py | 16 +- riocli/hwil/util.py | 14 +- riocli/hwilclient/__init__.py | 2 +- riocli/hwilclient/client.py | 65 +-- riocli/jsonschema/validate.py | 10 +- riocli/managedservice/__init__.py | 4 +- riocli/managedservice/delete.py | 10 +- riocli/managedservice/inspect.py | 15 +- riocli/managedservice/list.py | 4 +- riocli/managedservice/list_providers.py | 8 +- riocli/model/__init__.py | 2 +- riocli/model/base.py | 11 +- riocli/network/delete.py | 64 +-- riocli/network/inspect.py | 15 +- riocli/network/list.py | 41 +- riocli/network/model.py | 8 +- riocli/network/util.py | 18 +- riocli/organization/inspect.py | 47 +- riocli/organization/invite_user.py | 19 +- riocli/organization/list.py | 15 +- riocli/organization/remove_user.py | 22 +- riocli/organization/select.py | 58 ++- riocli/organization/users.py | 22 +- riocli/organization/utils.py | 34 +- riocli/package/__init__.py | 4 +- riocli/package/delete.py | 98 ++-- riocli/package/deployment.py | 23 +- riocli/package/enum.py | 2 +- riocli/package/inspect.py | 26 +- riocli/package/list.py | 33 +- riocli/package/model.py | 15 +- riocli/package/util.py | 46 +- riocli/parameter/apply.py | 102 ++-- riocli/parameter/delete.py | 35 +- riocli/parameter/diff.py | 67 ++- riocli/parameter/download.py | 52 +- riocli/parameter/list.py | 4 +- riocli/parameter/upload.py | 59 ++- riocli/parameter/utils.py | 49 +- riocli/project/__init__.py | 4 +- riocli/project/create.py | 37 +- riocli/project/delete.py | 29 +- riocli/project/features/vpn.py | 36 +- riocli/project/inspect.py | 16 +- riocli/project/list.py | 71 ++- riocli/project/model.py | 22 +- riocli/project/select.py | 31 +- riocli/project/update_owner.py | 49 +- riocli/project/util.py | 39 +- riocli/project/whoami.py | 38 +- riocli/rosbag/__init__.py | 4 +- riocli/rosbag/blob.py | 111 +++-- riocli/rosbag/job.py | 297 ++++++++---- riocli/rosbag/util.py | 2 +- riocli/secret/delete.py | 76 +-- riocli/secret/inspect.py | 13 +- riocli/secret/list.py | 32 +- riocli/secret/util.py | 35 +- riocli/shell/__init__.py | 14 +- riocli/shell/prompt.py | 6 +- riocli/static_route/create.py | 16 +- riocli/static_route/delete.py | 76 +-- riocli/static_route/inspect.py | 20 +- riocli/static_route/list.py | 33 +- riocli/static_route/model.py | 2 +- riocli/static_route/open.py | 6 +- riocli/static_route/util.py | 35 +- riocli/usergroup/delete.py | 35 +- riocli/usergroup/inspect.py | 52 +- riocli/usergroup/list.py | 10 +- riocli/usergroup/model.py | 98 ++-- riocli/usergroup/util.py | 12 +- riocli/utils/__init__.py | 102 ++-- riocli/utils/execute.py | 50 +- riocli/utils/graph.py | 38 +- riocli/utils/selector.py | 34 +- riocli/utils/spinner.py | 12 +- riocli/utils/ssh_tunnel.py | 8 +- riocli/utils/state.py | 16 +- riocli/v2client/__init__.py | 2 +- riocli/v2client/client.py | 617 ++++++++++++++---------- riocli/v2client/constants.py | 156 +++--- riocli/v2client/error.py | 6 +- riocli/v2client/util.py | 47 +- riocli/vpn/__init__.py | 4 +- riocli/vpn/connect.py | 100 ++-- riocli/vpn/disconnect.py | 25 +- riocli/vpn/machines.py | 55 ++- riocli/vpn/ping.py | 24 +- riocli/vpn/status.py | 81 ++-- riocli/vpn/util.py | 159 +++--- 179 files changed, 5379 insertions(+), 3677 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6735e354..60b14f7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,4 +35,11 @@ New dependencies can be installed directly using `uv`. This modifies the ``` bash uv add +``` + +### Linting and formatting +You can check and fix the code style by running the following commands. +```bash +uvx ruff check --fix +uvx ruff format ``` \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 181a85e4..0b1a3212 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -2,41 +2,40 @@ import os import sys -project = u'CLI' -copyright = u'2024, Rapyuta Robotics' -author = u'Rapyuta Robotics' +project = "CLI" +copyright = "2024, Rapyuta Robotics" +author = "Rapyuta Robotics" -sys.path.insert(0, os.path.abspath('../..')) +sys.path.insert(0, os.path.abspath("../..")) -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.autosummary', - 'sphinx.ext.ifconfig', - 'sphinx_click'] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.autosummary", + "sphinx.ext.ifconfig", + "sphinx_click", +] -templates_path = ['_templates'] +templates_path = ["_templates"] autosummary_generate = True # Turn on sphinx.ext.autosummary -source_suffix = '.rst' -master_doc = 'index' -language = 'en' +source_suffix = ".rst" +master_doc = "index" +language = "en" exclude_patterns = [] todo_include_todos = False -html_theme = 'furo' -html_favicon = 'favicon.ico' -html_static_path = ['_static'] +html_theme = "furo" +html_favicon = "favicon.ico" +html_static_path = ["_static"] html_theme_options = { "light_logo": "logo-light-mode.svg", "dark_logo": "logo-dark-mode.svg", } -html_css_files = ['css/rio-sphinx.css'] -html_js_files = ['js/rio-sphinx.js'] -htmlhelp_basename = 'RIOdoc' -man_pages = [ - (master_doc, 'cli', u'Rapyuta IO CLI', - [author], 1) -] +html_css_files = ["css/rio-sphinx.css"] +html_js_files = ["js/rio-sphinx.js"] +htmlhelp_basename = "RIOdoc" +man_pages = [(master_doc, "cli", "Rapyuta IO CLI", [author], 1)] intersphinx_mapping = { - 'python': ('https://docs.python.org/3/', None), + "python": ("https://docs.python.org/3/", None), } add_module_names = False diff --git a/docs/source/rapyuta/__init__.py b/docs/source/rapyuta/__init__.py index 0433ff76..5dde8816 100644 --- a/docs/source/rapyuta/__init__.py +++ b/docs/source/rapyuta/__init__.py @@ -3,9 +3,10 @@ From https://github.com/ryan-roemer/sphinx-bootstrap-theme. """ + import os -__version__ = '0.2.5b1' +__version__ = "0.2.5b1" __version_full__ = __version__ diff --git a/riocli/__init__.py b/riocli/__init__.py index dd6b3e1a..45444c67 100644 --- a/riocli/__init__.py +++ b/riocli/__init__.py @@ -23,6 +23,7 @@ # on windows. from sys import platform -if platform.lower() == 'win32': +if platform.lower() == "win32": import signal + signal.SIGKILL = signal.SIGTERM diff --git a/riocli/apply/__init__.py b/riocli/apply/__init__.py index 0cdbd5a2..f386c437 100644 --- a/riocli/apply/__init__.py +++ b/riocli/apply/__init__.py @@ -17,53 +17,91 @@ import click from click_help_colors import HelpColorsCommand -from riocli.apply.explain import explain, list_examples from riocli.apply.parse import Applier -from riocli.apply.template import template from riocli.apply.util import process_files_values_secrets from riocli.constants import Colors from riocli.utils import print_centered_text @click.command( - 'apply', + "apply", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--dryrun', '-d', is_flag=True, default=False, - help='Dry run the yaml files without applying any change') -@click.option('--show-graph', '-g', is_flag=True, default=False, - help='Opens a mermaid.live dependency graph') -@click.option('--values', '-v', multiple=True, default=(), - help="Path to values yaml file. Key/values " - "specified in the values file can be " - "used as variables in template YAMLs") -@click.option('--secrets', '-s', multiple=True, default=(), - help="Secret files are sops encoded value files. " - "rio-cli expects sops to be authorized for " - "decoding files on this computer") -@click.option('--workers', '-w', - help="Number of parallel workers while running apply " - "command. defaults to 6.", type=int) -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, - type=click.BOOL, default=False, - help="Skip confirmation") -@click.option('--retry-count', '-rc', type=int, default=50, - help="Number of retries before a resource creation times out status, defaults to 50") -@click.option('--retry-interval', '-ri', type=int, default=6, - help="Interval between retries defaults to 6") -@click.argument('files', nargs=-1) +@click.option( + "--dryrun", + "-d", + is_flag=True, + default=False, + help="Dry run the yaml files without applying any change", +) +@click.option( + "--show-graph", + "-g", + is_flag=True, + default=False, + help="Opens a mermaid.live dependency graph", +) +@click.option( + "--values", + "-v", + multiple=True, + default=(), + help="Path to values yaml file. Key/values " + "specified in the values file can be " + "used as variables in template YAMLs", +) +@click.option( + "--secrets", + "-s", + multiple=True, + default=(), + help="Secret files are sops encoded value files. " + "rio-cli expects sops to be authorized for " + "decoding files on this computer", +) +@click.option( + "--workers", + "-w", + help="Number of parallel workers while running apply " "command. defaults to 6.", + type=int, +) +@click.option( + "-f", + "--force", + "--silent", + "silent", + is_flag=True, + type=click.BOOL, + default=False, + help="Skip confirmation", +) +@click.option( + "--retry-count", + "-rc", + type=int, + default=50, + help="Number of retries before a resource creation times out status, defaults to 50", +) +@click.option( + "--retry-interval", + "-ri", + type=int, + default=6, + help="Interval between retries defaults to 6", +) +@click.argument("files", nargs=-1) def apply( - values: Iterable[str], - secrets: Iterable[str], - files: Iterable[str], - retry_count: int = 50, - retry_interval: int = 6, - dryrun: bool = False, - workers: int = 6, - silent: bool = False, - show_graph: bool = False, + values: Iterable[str], + secrets: Iterable[str], + files: Iterable[str], + retry_count: int = 50, + retry_interval: int = 6, + dryrun: bool = False, + workers: int = 6, + silent: bool = False, + show_graph: bool = False, ) -> None: """Apply resource manifests. @@ -110,13 +148,14 @@ def apply( $ rio apply -v values1.yaml -v values2.yaml templates/** """ glob_files, abs_values, abs_secrets = process_files_values_secrets( - files, values, secrets) + files, values, secrets + ) if len(glob_files) == 0: - click.secho('No files specified', fg=Colors.RED) + click.secho("No files specified", fg=Colors.RED) raise SystemExit(1) - print_centered_text('Files Processed') + print_centered_text("Files Processed") for file in glob_files: click.secho(file, fg=Colors.YELLOW) @@ -124,8 +163,7 @@ def apply( applier.parse_dependencies() if show_graph and dryrun: - click.secho('You cannot dry run and launch the graph together.', - fg='yellow') + click.secho("You cannot dry run and launch the graph together.", fg="yellow") return if show_graph: @@ -135,44 +173,84 @@ def apply( if not silent and not dryrun: click.confirm("\nDo you want to proceed?", default=True, abort=True) - print_centered_text('Applying Manifests') - applier.apply(dryrun=dryrun, workers=workers, retry_count=retry_count, retry_interval=retry_interval) + print_centered_text("Applying Manifests") + applier.apply( + dryrun=dryrun, + workers=workers, + retry_count=retry_count, + retry_interval=retry_interval, + ) @click.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--dryrun', '-d', is_flag=True, default=False, - help='Dry run the yaml files without applying any change') -@click.option('--values', '-v', multiple=True, default=(), - help="Path to values yaml file. key/values specified in the" - " values file can be used as variables in template YAMLs") -@click.option('--secrets', '-s', multiple=True, default=(), - help="Secret files are sops encoded value files. riocli expects " - "sops to be authorized for decoding files on this computer") -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, - type=click.BOOL, default=False, - help="Skip confirmation") -@click.option('--workers', '-w', - help="Number of parallel workers while running apply " - "command. defaults to 6.", type=int) -@click.option('--retry-count', '-rc', type=int, default=50, - help="Number of retries before a resource creation times out status, defaults to 50") -@click.option('--retry-interval', '-ri', type=int, default=6, - help="Interval between retries defaults to 6") -@click.argument('files', nargs=-1) +@click.option( + "--dryrun", + "-d", + is_flag=True, + default=False, + help="Dry run the yaml files without applying any change", +) +@click.option( + "--values", + "-v", + multiple=True, + default=(), + help="Path to values yaml file. key/values specified in the" + " values file can be used as variables in template YAMLs", +) +@click.option( + "--secrets", + "-s", + multiple=True, + default=(), + help="Secret files are sops encoded value files. riocli expects " + "sops to be authorized for decoding files on this computer", +) +@click.option( + "-f", + "--force", + "--silent", + "silent", + is_flag=True, + type=click.BOOL, + default=False, + help="Skip confirmation", +) +@click.option( + "--workers", + "-w", + help="Number of parallel workers while running apply " "command. defaults to 6.", + type=int, +) +@click.option( + "--retry-count", + "-rc", + type=int, + default=50, + help="Number of retries before a resource creation times out status, defaults to 50", +) +@click.option( + "--retry-interval", + "-ri", + type=int, + default=6, + help="Interval between retries defaults to 6", +) +@click.argument("files", nargs=-1) def delete( - values: str, - secrets: str, - files: Iterable[str], - retry_count: int = 50, - retry_interval: int = 6, - dryrun: bool = False, - workers: int = 6, - silent: bool = False + values: str, + secrets: str, + files: Iterable[str], + retry_count: int = 50, + retry_interval: int = 6, + dryrun: bool = False, + workers: int = 6, + silent: bool = False, ) -> None: """Removes resources via manifests @@ -215,13 +293,14 @@ def delete( $ rio delete -v values1.yaml -v values2.yaml templates/** """ glob_files, abs_values, abs_secrets = process_files_values_secrets( - files, values, secrets) + files, values, secrets + ) if len(glob_files) == 0: - click.secho('no files specified', fg=Colors.RED) + click.secho("no files specified", fg=Colors.RED) raise SystemExit(1) - print_centered_text('Files Processed') + print_centered_text("Files Processed") for file in glob_files: click.secho(file, fg=Colors.YELLOW) @@ -231,5 +310,10 @@ def delete( if not silent and not dryrun: click.confirm("\nDo you want to proceed?", default=True, abort=True) - print_centered_text('Deleting Resources') - applier.delete(dryrun=dryrun, workers=workers, retry_count=retry_count, retry_interval=retry_interval) + print_centered_text("Deleting Resources") + applier.delete( + dryrun=dryrun, + workers=workers, + retry_count=retry_count, + retry_interval=retry_interval, + ) diff --git a/riocli/apply/explain.py b/riocli/apply/explain.py index af909559..443e80c9 100644 --- a/riocli/apply/explain.py +++ b/riocli/apply/explain.py @@ -14,7 +14,6 @@ from pathlib import Path import click -import yaml from click_help_colors import HelpColorsCommand from riocli.constants import Colors, Symbols @@ -22,33 +21,32 @@ @click.command( - 'list-examples', + "list-examples", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, - help='List all examples supported in rio explain command' + help="List all examples supported in rio explain command", ) def list_examples() -> None: """List all examples supported in rio explain command.""" - path = Path(__file__).parent.joinpath('manifests') + path = Path(__file__).parent.joinpath("manifests") examples = [] - for each in path.glob('*.yaml'): - examples.append([each.name.split('.yaml')[0]]) + for each in path.glob("*.yaml"): + examples.append([each.name.split(".yaml")[0]]) - tabulate_data(examples, ['Examples']) + tabulate_data(examples, ["Examples"]) @click.command( - 'explain', + "explain", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, - help='Generates a sample resource manifest for the given type' + help="Generates a sample resource manifest for the given type", ) -@click.option('--templates', help='Alternate root for templates', - default=None) -@click.argument('resource') +@click.option("--templates", help="Alternate root for templates", default=None) +@click.argument("resource") def explain(resource: str, templates: str = None) -> None: """Explain a resource manifest for the given type. @@ -71,14 +69,15 @@ def explain(resource: str, templates: str = None) -> None: if templates: path = Path(templates) else: - path = Path(__file__).parent.joinpath('manifests') + path = Path(__file__).parent.joinpath("manifests") - for each in path.glob('**/*'): - if resource + '.yaml' == each.name: + for each in path.glob("**/*"): + if resource + ".yaml" == each.name: with open(each) as f: click.echo_via_pager(f.readlines()) raise SystemExit(0) - click.secho('{} Resource "{}" not found'.format(Symbols.ERROR, resource), - fg=Colors.RED) + click.secho( + '{} Resource "{}" not found'.format(Symbols.ERROR, resource), fg=Colors.RED + ) raise SystemExit(1) diff --git a/riocli/apply/filters.py b/riocli/apply/filters.py index b5c472c3..01a65c8c 100644 --- a/riocli/apply/filters.py +++ b/riocli/apply/filters.py @@ -37,5 +37,5 @@ def getenv(default: str, env_var: str) -> str: FILTERS = { - 'getenv': getenv, + "getenv": getenv, } diff --git a/riocli/apply/parse.py b/riocli/apply/parse.py index 93ee81d1..1e86574f 100644 --- a/riocli/apply/parse.py +++ b/riocli/apply/parse.py @@ -15,13 +15,18 @@ import queue import threading import typing -from graphlib import TopologicalSorter import click import yaml +from graphlib import TopologicalSorter from munch import munchify -from riocli.apply.util import (get_model, init_jinja_environment, message_with_prompt, print_resolved_objects) +from riocli.apply.util import ( + get_model, + init_jinja_environment, + message_with_prompt, + print_resolved_objects, +) from riocli.config import Configuration from riocli.constants import Colors, Symbols, ApplyResult from riocli.exceptions import ResourceNotFound @@ -32,7 +37,7 @@ class Applier(object): DEFAULT_MAX_WORKERS = 6 - DELETE_POLICY_LABEL = 'rapyuta.io/deletionPolicy' + DELETE_POLICY_LABEL = "rapyuta.io/deletionPolicy" def __init__(self, files: typing.List, values: typing.List, secrets: typing.List): self.files = {} @@ -42,7 +47,7 @@ def __init__(self, files: typing.List, values: typing.List, secrets: typing.List self.config = Configuration() self.graph = TopologicalSorter() self.environment = init_jinja_environment() - self.diagram = Graphviz(direction='LR', format='svg') + self.diagram = Graphviz(direction="LR", format="svg") self._process_values_and_secrets(values, secrets) self._process_file_list(files) @@ -56,23 +61,24 @@ def print_resolved_manifests(self): dump_all_yaml(manifests) - @with_spinner(text='Applying...', timer=True) + @with_spinner(text="Applying...", timer=True) def apply(self, *args, **kwargs): """Apply the resources defined in the manifest files""" - spinner = kwargs.get('spinner') - kwargs['workers'] = int(kwargs.get('workers') - or self.DEFAULT_MAX_WORKERS) + spinner = kwargs.get("spinner") + kwargs["workers"] = int(kwargs.get("workers") or self.DEFAULT_MAX_WORKERS) apply_func = self.apply_async - if kwargs['workers'] == 1: + if kwargs["workers"] == 1: apply_func = self.apply_sync try: apply_func(*args, **kwargs) - spinner.text = click.style('Apply successful.', fg=Colors.BRIGHT_GREEN) + spinner.text = click.style("Apply successful.", fg=Colors.BRIGHT_GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style('Apply failed. Error: {}'.format(e), fg=Colors.BRIGHT_RED) + spinner.text = click.style( + "Apply failed. Error: {}".format(e), fg=Colors.BRIGHT_RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -80,8 +86,10 @@ def apply_sync(self, *args, **kwargs): self.graph.prepare() while self.graph.is_active(): for obj in self.graph.get_ready(): - if (obj in self.resolved_objects and - 'manifest' in self.resolved_objects[obj]): + if ( + obj in self.resolved_objects + and "manifest" in self.resolved_objects[obj] + ): self._apply_manifest(obj, *args, **kwargs) self.graph.done(obj) @@ -90,10 +98,7 @@ def apply_async(self, *args, **kwargs): done_queue = queue.Queue() self._start_apply_workers( - self._apply_manifest, - task_queue, - done_queue, - *args, **kwargs + self._apply_manifest, task_queue, done_queue, *args, **kwargs ) self.graph.prepare() @@ -110,23 +115,24 @@ def apply_async(self, *args, **kwargs): # Block until the task_queue is empty. task_queue.join() - @with_spinner(text='Deleting...', timer=True) + @with_spinner(text="Deleting...", timer=True) def delete(self, *args, **kwargs): """Delete resources defined in manifests.""" - spinner = kwargs.get('spinner') - kwargs['workers'] = int(kwargs.get('workers') - or self.DEFAULT_MAX_WORKERS) + spinner = kwargs.get("spinner") + kwargs["workers"] = int(kwargs.get("workers") or self.DEFAULT_MAX_WORKERS) delete_func = self.delete_async - if kwargs['workers'] == 1: + if kwargs["workers"] == 1: delete_func = self.delete_sync try: delete_func(*args, **kwargs) - spinner.text = click.style('Delete successful.', fg=Colors.BRIGHT_GREEN) + spinner.text = click.style("Delete successful.", fg=Colors.BRIGHT_GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style('Delete failed. Error: {}'.format(e), fg=Colors.BRIGHT_RED) + spinner.text = click.style( + "Delete failed. Error: {}".format(e), fg=Colors.BRIGHT_RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -134,7 +140,7 @@ def delete_sync(self, *args, **kwargs) -> None: delete_order = list(self.graph.static_order()) delete_order.reverse() for o in delete_order: - if o in self.resolved_objects and 'manifest' in self.resolved_objects[o]: + if o in self.resolved_objects and "manifest" in self.resolved_objects[o]: self._delete_manifest(o, *args, **kwargs) def delete_async(self, *args, **kwargs) -> None: @@ -142,10 +148,7 @@ def delete_async(self, *args, **kwargs) -> None: done_queue = queue.Queue() self._start_apply_workers( - self._delete_manifest, - task_queue, - done_queue, - *args, **kwargs + self._delete_manifest, task_queue, done_queue, *args, **kwargs ) for nodes in self._get_async_delete_order(): @@ -159,11 +162,12 @@ def delete_async(self, *args, **kwargs) -> None: task_queue.join() def _start_apply_workers( - self, - func: typing.Callable, - tasks: queue.Queue, - done: queue.Queue, - *args, **kwargs, + self, + func: typing.Callable, + tasks: queue.Queue, + done: queue.Queue, + *args, + **kwargs, ) -> None: """A helper method to start workers for apply/delete operations @@ -173,10 +177,11 @@ def _start_apply_workers( The `tasks` queue is used to pass objects to the workers for processing. The `done` queue is used to pass the processed objects back to the main. """ + def _worker(): while True: o = tasks.get() - if o in self.resolved_objects and 'manifest' in self.resolved_objects[o]: + if o in self.resolved_objects and "manifest" in self.resolved_objects[o]: try: func(o, *args, **kwargs) except Exception as ex: @@ -192,13 +197,9 @@ def _worker(): # in the done_queue. The main thread will wait for the task_queue # to be empty before exiting. The daemon threads will die with the # main process. - n = int(kwargs.get('workers') or self.DEFAULT_MAX_WORKERS) + n = int(kwargs.get("workers") or self.DEFAULT_MAX_WORKERS) for i in range(n): - threading.Thread( - target=_worker, - daemon=True, - name=f'worker-{i}' - ).start() + threading.Thread(target=_worker, daemon=True, name=f"worker-{i}").start() def _get_async_delete_order(self): """Returns the delete order for async delete operation @@ -233,7 +234,7 @@ def show_dependency_graph(self): def _apply_manifest(self, obj_key: str, *args, **kwargs) -> None: """Instantiate and apply the object manifest""" - spinner = kwargs.get('spinner') + spinner = kwargs.get("spinner") dryrun = kwargs.get("dryrun", False) obj = self.objects[obj_key] @@ -242,14 +243,17 @@ def _apply_manifest(self, obj_key: str, *args, **kwargs) -> None: try: kls.validate(obj) except Exception as ex: - raise Exception(f'invalid manifest {obj_key}: {str(ex)}') + raise Exception(f"invalid manifest {obj_key}: {str(ex)}") ist = kls(munchify(obj)) obj_key = click.style(obj_key, bold=True) - message_with_prompt("{} Applying {}...".format( - Symbols.WAITING, obj_key), fg=Colors.CYAN, spinner=spinner) + message_with_prompt( + "{} Applying {}...".format(Symbols.WAITING, obj_key), + fg=Colors.CYAN, + spinner=spinner, + ) try: result = ApplyResult.CREATED @@ -257,21 +261,31 @@ def _apply_manifest(self, obj_key: str, *args, **kwargs) -> None: result = ist.apply(*args, **kwargs) if result == ApplyResult.EXISTS: - message_with_prompt("{} {} already exists".format( - Symbols.INFO, obj_key), fg=Colors.WHITE, spinner=spinner) + message_with_prompt( + "{} {} already exists".format(Symbols.INFO, obj_key), + fg=Colors.WHITE, + spinner=spinner, + ) return - message_with_prompt("{} {} {}".format( - Symbols.SUCCESS, result, obj_key), - fg=Colors.GREEN, spinner=spinner) + message_with_prompt( + "{} {} {}".format(Symbols.SUCCESS, result, obj_key), + fg=Colors.GREEN, + spinner=spinner, + ) except Exception as ex: - message_with_prompt("{} Failed to apply {}. Error: {}".format( - Symbols.ERROR, obj_key, str(ex)), fg=Colors.RED, spinner=spinner) - raise Exception(f'{obj_key}: {str(ex)}') + message_with_prompt( + "{} Failed to apply {}. Error: {}".format( + Symbols.ERROR, obj_key, str(ex) + ), + fg=Colors.RED, + spinner=spinner, + ) + raise Exception(f"{obj_key}: {str(ex)}") def _delete_manifest(self, obj_key: str, *args, **kwargs) -> None: """Instantiate and delete the object manifest""" - spinner = kwargs.get('spinner') + spinner = kwargs.get("spinner") dryrun = kwargs.get("dryrun", False) obj = self.objects[obj_key] @@ -280,39 +294,58 @@ def _delete_manifest(self, obj_key: str, *args, **kwargs) -> None: try: kls.validate(obj) except Exception as ex: - raise Exception(f'invalid manifest {obj_key}: {str(ex)}') + raise Exception(f"invalid manifest {obj_key}: {str(ex)}") ist = kls(munchify(obj)) obj_key = click.style(obj_key, bold=True) - message_with_prompt("{} Deleting {}...".format( - Symbols.WAITING, obj_key), fg=Colors.CYAN, spinner=spinner) + message_with_prompt( + "{} Deleting {}...".format(Symbols.WAITING, obj_key), + fg=Colors.CYAN, + spinner=spinner, + ) # If a resource has a label with DELETE_POLICY_LABEL set # to 'retain', it should not be deleted. - labels = obj.get('metadata', {}).get('labels', {}) - can_delete = labels.get(self.DELETE_POLICY_LABEL) != 'retain' + labels = obj.get("metadata", {}).get("labels", {}) + can_delete = labels.get(self.DELETE_POLICY_LABEL) != "retain" if not can_delete: - message_with_prompt("{} {} cannot be deleted since deletion policy is set to 'retain'".format( - Symbols.INFO, obj_key), fg=Colors.WHITE, spinner=spinner) + message_with_prompt( + "{} {} cannot be deleted since deletion policy is set to 'retain'".format( + Symbols.INFO, obj_key + ), + fg=Colors.WHITE, + spinner=spinner, + ) return try: if not dryrun and can_delete: ist.delete(*args, **kwargs) - message_with_prompt("{} Deleted {}".format( - Symbols.SUCCESS, obj_key), fg=Colors.GREEN, spinner=spinner) + message_with_prompt( + "{} Deleted {}".format(Symbols.SUCCESS, obj_key), + fg=Colors.GREEN, + spinner=spinner, + ) except ResourceNotFound: - message_with_prompt("{} {} not found".format( - Symbols.WARNING, obj_key), fg=Colors.YELLOW, spinner=spinner) + message_with_prompt( + "{} {} not found".format(Symbols.WARNING, obj_key), + fg=Colors.YELLOW, + spinner=spinner, + ) return except Exception as ex: - message_with_prompt("{} Failed to delete {}. Error: {}".format( - Symbols.ERROR, obj_key, str(ex)), fg=Colors.RED, spinner=spinner) - raise Exception(f'{obj_key}: {str(ex)}') + message_with_prompt( + "{} Failed to delete {}. Error: {}".format( + Symbols.ERROR, obj_key, str(ex) + ), + fg=Colors.RED, + spinner=spinner, + ) + raise Exception(f"{obj_key}: {str(ex)}") def _process_file_list(self, files): for f in files: @@ -327,7 +360,7 @@ def _register_object(self, data): try: key = self._get_object_key(data) self.objects[key] = data - self.resolved_objects[key] = {'src': 'local', 'manifest': data} + self.resolved_objects[key] = {"src": "local", "manifest": data} except KeyError: click.secho("Key error {}".format(data), fg=Colors.RED) return @@ -339,51 +372,51 @@ def _load_file_content(self, file_name, is_value=False, is_secret=False): """ try: if is_secret: - data = run_bash(f'sops -d {file_name}') + data = run_bash(f"sops -d {file_name}") else: with open(file_name) as f: data = f.read() except Exception as e: - raise Exception(f'Error loading file {file_name}: {str(e)}') + raise Exception(f"Error loading file {file_name}: {str(e)}") # When the file is a template, render it using # values or secrets. if not (is_value or is_secret): - if self.environment or file_name.endswith('.j2'): + if self.environment or file_name.endswith(".j2"): try: template = self.environment.from_string(data) except Exception as e: - raise Exception(f'Error loading template {file_name}: {str(e)}') + raise Exception(f"Error loading template {file_name}: {str(e)}") template_args = self.values if self.secrets: - template_args['secrets'] = self.secrets + template_args["secrets"] = self.secrets try: data = template.render(**template_args) except Exception as ex: - raise Exception(f'Failed to parse {file_name}: {str(ex)}') + raise Exception(f"Failed to parse {file_name}: {str(ex)}") - file_name = file_name.rstrip('.j2') + file_name = file_name.rstrip(".j2") loaded_data = [] - if file_name.endswith('json'): + if file_name.endswith("json"): # FIXME: Handle for JSON List. try: loaded = json.loads(data) loaded_data.append(loaded) except json.JSONDecodeError as ex: - raise Exception(f'Failed to parse {file_name}: {str(ex)}') - elif file_name.endswith('yaml') or file_name.endswith('yml'): + raise Exception(f"Failed to parse {file_name}: {str(ex)}") + elif file_name.endswith("yaml") or file_name.endswith("yml"): try: loaded = yaml.safe_load_all(data) loaded_data = list(loaded) except yaml.YAMLError as e: - raise Exception(f'Failed to parse {file_name}: {str(e)}') + raise Exception(f"Failed to parse {file_name}: {str(e)}") if not loaded_data: - click.secho('{} file is empty'.format(file_name)) + click.secho("{} file is empty".format(file_name)) return loaded_data @@ -403,11 +436,11 @@ def _parse_dependency(self, dependent_key, model): for key, value in model.items(): if key == "depends": - if 'kind' in value and value.get('kind'): + if "kind" in value and value.get("kind"): self._resolve_dependency(dependent_key, value) if isinstance(value, list): for each in value: - if isinstance(each, dict) and each.get('kind'): + if isinstance(each, dict) and each.get("kind"): self._resolve_dependency(dependent_key, each) continue @@ -422,46 +455,48 @@ def _parse_dependency(self, dependent_key, model): self._parse_dependency(dependent_key, each) def _resolve_dependency(self, dependent_key, dependency): - kind = dependency.get('kind') - name_or_guid = dependency.get('nameOrGUID') - key = '{}:{}'.format(kind, name_or_guid) + kind = dependency.get("kind") + name_or_guid = dependency.get("nameOrGUID") + key = "{}:{}".format(kind, name_or_guid) self._add_graph_edge(dependent_key, key) @staticmethod def _get_object_key(obj: dict) -> str: - kind = obj.get('kind').lower() - name_or_guid = obj.get('metadata', {}).get('name') + kind = obj.get("kind").lower() + name_or_guid = obj.get("metadata", {}).get("name") if not name_or_guid: - raise ValueError('[kind:{}] name is required.'.format(kind)) + raise ValueError("[kind:{}] name is required.".format(kind)) - return '{}:{}'.format(kind, name_or_guid) + return "{}:{}".format(kind, name_or_guid) def _inject_rio_namespace(self, values: typing.Optional[dict] = None) -> dict: values = values or {} rio = { - 'project': { - 'name': self.config.data.get('project_name'), - 'guid': self.config.project_guid, + "project": { + "name": self.config.data.get("project_name"), + "guid": self.config.project_guid, }, - 'organization': { - 'name': self.config.data.get('organization_name'), - 'guid': self.config.organization_guid, - 'short_id': self.config.organization_short_id, + "organization": { + "name": self.config.data.get("organization_name"), + "guid": self.config.organization_guid, + "short_id": self.config.organization_short_id, }, - 'email_id': self.config.data.get('email_id'), + "email_id": self.config.data.get("email_id"), } - if 'rio' in values: - values['rio'].update(rio) + if "rio" in values: + values["rio"].update(rio) else: - values['rio'] = rio + values["rio"] = rio return values - def _process_values_and_secrets(self, values: typing.List, secrets: typing.List) -> None: + def _process_values_and_secrets( + self, values: typing.List, secrets: typing.List + ) -> None: """Process the values and secrets files and inject them into the manifest files""" self.values, self.secrets = {}, {} diff --git a/riocli/apply/template.py b/riocli/apply/template.py index 1d483a2b..49318214 100644 --- a/riocli/apply/template.py +++ b/riocli/apply/template.py @@ -22,22 +22,32 @@ @click.command( - 'template', + "template", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--values', '-v', multiple=True, default=(), - help='Path to values yaml file. key/values specified in the ' - 'values file can be used as variables in template YAMLs') -@click.option('--secrets', '-s', multiple=True, default=(), - help='Secret files are sops encoded value files. riocli ' - 'expects sops to be authorized for decoding files on this computer') -@click.argument('files', nargs=-1) +@click.option( + "--values", + "-v", + multiple=True, + default=(), + help="Path to values yaml file. key/values specified in the " + "values file can be used as variables in template YAMLs", +) +@click.option( + "--secrets", + "-s", + multiple=True, + default=(), + help="Secret files are sops encoded value files. riocli " + "expects sops to be authorized for decoding files on this computer", +) +@click.argument("files", nargs=-1) def template( - values: typing.Tuple[str], - secrets: typing.Tuple[str], - files: typing.Tuple[str], + values: typing.Tuple[str], + secrets: typing.Tuple[str], + files: typing.Tuple[str], ) -> None: """Print manifests with values and secrets applied @@ -65,10 +75,11 @@ def template( rio template templates/** -v common.yaml -v site.yaml """ glob_files, abs_values, abs_secrets = process_files_values_secrets( - files, values, secrets) + files, values, secrets + ) if len(glob_files) == 0: - click.secho('No files specified', fg=Colors.RED) + click.secho("No files specified", fg=Colors.RED) raise SystemExit(1) applier = Applier(glob_files, abs_values, abs_secrets) diff --git a/riocli/apply/util.py b/riocli/apply/util.py index 4b15345e..fb969099 100644 --- a/riocli/apply/util.py +++ b/riocli/apply/util.py @@ -39,28 +39,28 @@ from riocli.utils import tabulate_data KIND_TO_CLASS = { - 'project': Project, - 'secret': Secret, - 'device': Device, - 'network': Network, - 'staticroute': StaticRoute, - 'package': Package, - 'disk': Disk, - 'deployment': Deployment, + "project": Project, + "secret": Secret, + "device": Device, + "network": Network, + "staticroute": StaticRoute, + "package": Package, + "disk": Disk, + "deployment": Deployment, "managedservice": ManagedService, - 'usergroup': UserGroup, + "usergroup": UserGroup, } def get_model(data: dict) -> Model: """Get the model class based on the kind""" - kind = data.get('kind', None) + kind = data.get("kind", None) if kind is None: - raise Exception('kind is missing') + raise Exception("kind is missing") klass = KIND_TO_CLASS.get(str(kind).lower(), None) if klass is None: - raise Exception('invalid kind {}'.format(kind)) + raise Exception("invalid kind {}".format(kind)) return klass @@ -86,17 +86,15 @@ def parse_variadic_path_args(path_item): def process_files_values_secrets( - files: Iterable[str], - values: Iterable[str], - secrets: Iterable[str], + files: Iterable[str], + values: Iterable[str], + secrets: Iterable[str], ): glob_files = [] for path_item in files: path_glob = parse_variadic_path_args(path_item) - glob_files.extend([ - f for f in path_glob if os.path.isfile(f) - ]) + glob_files.extend([f for f in path_glob if os.path.isfile(f)]) # Remove value files from template files list. abs_values = values @@ -119,18 +117,18 @@ def process_files_values_secrets( def message_with_prompt( - left_msg: str, - right_msg: str = '', - fg: str = Colors.WHITE, - spinner: Yaspin = None, + left_msg: str, + right_msg: str = "", + fg: str = Colors.WHITE, + spinner: Yaspin = None, ) -> None: """Prints a message with a prompt and a timestamp. >> left_msg spacer right_msg time """ columns, _ = get_terminal_size() - t = datetime.now().isoformat('T') - spacer = ' ' * (int(columns) - len(left_msg + right_msg + t) - 12) + t = datetime.now().isoformat("T") + spacer = " " * (int(columns) - len(left_msg + right_msg + t) - 12) text = click.style(f">> {left_msg}{spacer}{right_msg} [{t}]", fg=fg) printer = spinner.write if spinner else click.echo printer(text) @@ -139,10 +137,10 @@ def message_with_prompt( def print_resolved_objects(objects: typing.Dict) -> None: data = [] for o in objects: - kind, name = o.split(':') + kind, name = o.split(":") data.append([kind.title(), name]) - tabulate_data(data, headers=['Kind', 'Name']) + tabulate_data(data, headers=["Kind", "Name"]) def init_jinja_environment(): diff --git a/riocli/auth/login.py b/riocli/auth/login.py index c2bdc539..69c7f54e 100644 --- a/riocli/auth/login.py +++ b/riocli/auth/login.py @@ -24,43 +24,54 @@ from riocli.utils.context import get_root_context from riocli.vpn.util import cleanup_hosts_file -LOGIN_SUCCESS = click.style('{} Logged in successfully!'.format(Symbols.SUCCESS), fg=Colors.GREEN) +LOGIN_SUCCESS = click.style( + "{} Logged in successfully!".format(Symbols.SUCCESS), fg=Colors.GREEN +) @click.command( - 'login', + "login", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--email', type=str, - help='Email of the rapyuta.io account') -@click.option('--password', type=str, - help='Password for the rapyuta.io account') -@click.option('--organization', type=str, default=None, - help=('Context will be set to the organization after ' - 'authentication')) -@click.option('--project', type=str, default=None, - help='Context will be set to the project after authentication') -@click.option('--interactive/--no-interactive', '--interactive/--silent', - is_flag=True, type=bool, default=True, - help='Make login interactive') -@click.option('--auth-token', type=str, default=None, - help="Login with auth token only") +@click.option("--email", type=str, help="Email of the rapyuta.io account") +@click.option("--password", type=str, help="Password for the rapyuta.io account") +@click.option( + "--organization", + type=str, + default=None, + help=("Context will be set to the organization after " "authentication"), +) +@click.option( + "--project", + type=str, + default=None, + help="Context will be set to the project after authentication", +) +@click.option( + "--interactive/--no-interactive", + "--interactive/--silent", + is_flag=True, + type=bool, + default=True, + help="Make login interactive", +) +@click.option("--auth-token", type=str, default=None, help="Login with auth token only") @click.pass_context def login( - ctx: click.Context, - email: str, - password: str, - organization: str, - project: str, - interactive: bool, - auth_token: str, + ctx: click.Context, + email: str, + password: str, + organization: str, + project: str, + interactive: bool, + auth_token: str, ) -> None: """Log into your rapyuta.io account. This is the first step to start using the CLI. - + You can log in with your email and password or just with and auth token if you already have one. The command works in an interactive mode by default @@ -110,28 +121,29 @@ def login( raise SystemExit(1) else: if interactive: - email = email or click.prompt('Email') - password = password or click.prompt('Password', hide_input=True) + email = email or click.prompt("Email") + password = password or click.prompt("Password", hide_input=True) if not email: - click.secho('email not specified') + click.secho("email not specified") raise SystemExit(1) if not password: - click.secho('password not specified') + click.secho("password not specified") raise SystemExit(1) - ctx.obj.data['email_id'] = email - ctx.obj.data['auth_token'] = get_token(email, password) + ctx.obj.data["email_id"] = email + ctx.obj.data["auth_token"] = get_token(email, password) # Save if the file does not already exist if not ctx.obj.exists or not interactive: ctx.obj.save() else: click.confirm( - '{} Config already exists. Do you want to override' - ' the existing config?'.format(Symbols.WARNING), - abort=True) + "{} Config already exists. Do you want to override" + " the existing config?".format(Symbols.WARNING), + abort=True, + ) if not interactive: # When just the email and password are provided @@ -145,8 +157,9 @@ def login( # needs to be explicitly provided in this case. if project and not organization: click.secho( - 'Please specify an organization. See `rio auth login --help`', - fg=Colors.YELLOW) + "Please specify an organization. See `rio auth login --help`", + fg=Colors.YELLOW, + ) raise SystemExit(1) # When just the organization is provided, we save the @@ -154,8 +167,12 @@ def login( # successful. if organization and not project: select_organization(ctx.obj, organization=organization) - click.secho("Your organization is set to '{}'".format( - ctx.obj.data['organization_name']), fg=Colors.CYAN) + click.secho( + "Your organization is set to '{}'".format( + ctx.obj.data["organization_name"] + ), + fg=Colors.CYAN, + ) ctx.obj.save() click.echo(LOGIN_SUCCESS) return @@ -168,7 +185,9 @@ def login( try: cleanup_hosts_file() except Exception as e: - click.secho(f'{Symbols.WARNING} Failed to ' - f'clean up hosts file: {str(e)}', fg=Colors.YELLOW) + click.secho( + f"{Symbols.WARNING} Failed to " f"clean up hosts file: {str(e)}", + fg=Colors.YELLOW, + ) click.echo(LOGIN_SUCCESS) diff --git a/riocli/auth/logout.py b/riocli/auth/logout.py index 998dd697..c7becf1c 100644 --- a/riocli/auth/logout.py +++ b/riocli/auth/logout.py @@ -18,7 +18,7 @@ @click.command( - 'logout', + "logout", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, @@ -29,14 +29,14 @@ def logout(ctx: click.Context): if not ctx.obj.exists: return - ctx.obj.data.pop('email_id', None) - ctx.obj.data.pop('auth_token', None) - ctx.obj.data.pop('project_id', None) - ctx.obj.data.pop('project_name', None) - ctx.obj.data.pop('organization_id', None) - ctx.obj.data.pop('organization_name', None) - ctx.obj.data.pop('organization_short_id', None) + ctx.obj.data.pop("email_id", None) + ctx.obj.data.pop("auth_token", None) + ctx.obj.data.pop("project_id", None) + ctx.obj.data.pop("project_name", None) + ctx.obj.data.pop("organization_id", None) + ctx.obj.data.pop("organization_name", None) + ctx.obj.data.pop("organization_short_id", None) ctx.obj.save() - click.secho('{} Logged out successfully.'.format(Symbols.SUCCESS), fg=Colors.GREEN) + click.secho("{} Logged out successfully.".format(Symbols.SUCCESS), fg=Colors.GREEN) diff --git a/riocli/auth/refresh_token.py b/riocli/auth/refresh_token.py index 260682fd..1fe0cf54 100644 --- a/riocli/auth/refresh_token.py +++ b/riocli/auth/refresh_token.py @@ -21,16 +21,21 @@ @click.command( - 'refresh-token', + "refresh-token", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) @click.pass_context -@click.option('--password', type=str, help='Password for the rapyuta.io account') -@click.option('--interactive/--no-interactive', '--interactive/--silent', - is_flag=True, type=bool, default=True, - help='Make login interactive') +@click.option("--password", type=str, help="Password for the rapyuta.io account") +@click.option( + "--interactive/--no-interactive", + "--interactive/--silent", + is_flag=True, + type=bool, + default=True, + help="Make login interactive", +) def refresh_token(ctx: click.Context, password: str, interactive: bool): """Refreshes the authentication token. @@ -38,7 +43,7 @@ def refresh_token(ctx: click.Context, password: str, interactive: bool): user to enter the password to refresh the token. """ config = get_config_from_context(ctx) - email = config.data.get('email_id', None) + email = config.data.get("email_id", None) try: if not config.exists or email is None: @@ -47,21 +52,24 @@ def refresh_token(ctx: click.Context, password: str, interactive: bool): click.secho(str(e), fg=Colors.RED) raise SystemExit(1) from e - click.secho(f'Refreshing token for {email}...', fg=Colors.YELLOW) + click.secho(f"Refreshing token for {email}...", fg=Colors.YELLOW) - existing_token = config.data.get('auth_token') + existing_token = config.data.get("auth_token") refreshed = api_refresh_token(existing_token) if not refreshed: if not interactive and password is None: - click.secho('The existing token has expired, re-run rio auth refresh-token ' - 'in interactive mode or pass the password using the flag.') + click.secho( + "The existing token has expired, re-run rio auth refresh-token " + "in interactive mode or pass the password using the flag." + ) raise SystemExit(1) - password = password or click.prompt('Password', hide_input=True) + password = password or click.prompt("Password", hide_input=True) refreshed = get_token(email, password) - ctx.obj.data['auth_token'] = refreshed + ctx.obj.data["auth_token"] = refreshed ctx.obj.save() - click.secho('{} Token refreshed successfully!'.format(Symbols.SUCCESS), - fg=Colors.GREEN) + click.secho( + "{} Token refreshed successfully!".format(Symbols.SUCCESS), fg=Colors.GREEN + ) diff --git a/riocli/auth/staging.py b/riocli/auth/staging.py index 84013bd6..47d5e486 100644 --- a/riocli/auth/staging.py +++ b/riocli/auth/staging.py @@ -21,11 +21,16 @@ _NAMED_ENVIRONMENTS = ["v11", "v12", "v13", "v14", "v15", "qa", "dev"] -@click.command('environment', hidden=True) -@click.option('--interactive/--no-interactive', '--interactive/--silent', - is_flag=True, type=bool, default=True, - help='Make login interactive') -@click.argument('name', type=str) +@click.command("environment", hidden=True) +@click.option( + "--interactive/--no-interactive", + "--interactive/--silent", + is_flag=True, + type=bool, + default=True, + help="Make login interactive", +) +@click.argument("name", type=str) @click.pass_context def environment(ctx: click.Context, interactive: bool, name: str): """ @@ -33,57 +38,64 @@ def environment(ctx: click.Context, interactive: bool, name: str): """ ctx = get_root_context(ctx) - if name == 'ga': - ctx.obj.data.pop('environment', None) - ctx.obj.data.pop('catalog_host', None) - ctx.obj.data.pop('core_api_host', None) - ctx.obj.data.pop('rip_host', None) - ctx.obj.data.pop('v2api_host', None) + if name == "ga": + ctx.obj.data.pop("environment", None) + ctx.obj.data.pop("catalog_host", None) + ctx.obj.data.pop("core_api_host", None) + ctx.obj.data.pop("rip_host", None) + ctx.obj.data.pop("v2api_host", None) else: _configure_environment(ctx.obj, name) # Remove all relevant data - ctx.obj.data.pop('project_id', None) - ctx.obj.data.pop('organization_id', None) - ctx.obj.data.pop('auth_token', None) + ctx.obj.data.pop("project_id", None) + ctx.obj.data.pop("organization_id", None) + ctx.obj.data.pop("auth_token", None) ctx.obj.save() - success_msg = click.style('{} Your Rapyuta.io environment is set to {}'.format(Symbols.SUCCESS, name), - fg=Colors.GREEN) + success_msg = click.style( + "{} Your Rapyuta.io environment is set to {}".format(Symbols.SUCCESS, name), + fg=Colors.GREEN, + ) if not interactive: click.echo(success_msg) click.secho( - '{} Please set your organization and project with' - ' `rio organization select ORGANIZATION_NAME`'.format(Symbols.WARNING), - fg=Colors.YELLOW + "{} Please set your organization and project with" + " `rio organization select ORGANIZATION_NAME`".format(Symbols.WARNING), + fg=Colors.YELLOW, ) return - ctx.obj.data['email_id'] = None - ctx.obj.data['auth_token'] = None + ctx.obj.data["email_id"] = None + ctx.obj.data["auth_token"] = None ctx.obj.save() - click.secho(f'Your environment is set to {name}. Please login again using `rio auth login`', fg=Colors.GREEN) + click.secho( + f"Your environment is set to {name}. Please login again using `rio auth login`", + fg=Colors.GREEN, + ) def _configure_environment(config: Configuration, name: str) -> None: - is_valid_env = name in _NAMED_ENVIRONMENTS or name.startswith('pr') + is_valid_env = name in _NAMED_ENVIRONMENTS or name.startswith("pr") if not is_valid_env: - click.secho('{} Invalid environment: {}'.format(Symbols.ERROR, name), fg=Colors.RED) + click.secho( + "{} Invalid environment: {}".format(Symbols.ERROR, name), fg=Colors.RED + ) raise SystemExit(1) subdomain = _STAGING_ENVIRONMENT_SUBDOMAIN - catalog = 'https://{}catalog.{}'.format(name, subdomain) - core = 'https://{}apiserver.{}'.format(name, subdomain) - rip = 'https://{}rip.{}'.format(name, subdomain) - v2api = 'https://{}api.{}'.format(name, subdomain) + catalog = "https://{}catalog.{}".format(name, subdomain) + core = "https://{}apiserver.{}".format(name, subdomain) + rip = "https://{}rip.{}".format(name, subdomain) + v2api = "https://{}api.{}".format(name, subdomain) - config.data['environment'] = name - config.data['catalog_host'] = catalog - config.data['core_api_host'] = core - config.data['rip_host'] = rip - config.data['v2api_host'] = v2api + config.data["environment"] = name + config.data["catalog_host"] = catalog + config.data["core_api_host"] = core + config.data["rip_host"] = rip + config.data["v2api_host"] = v2api diff --git a/riocli/auth/status.py b/riocli/auth/status.py index 5d404a55..2d9de1a2 100644 --- a/riocli/auth/status.py +++ b/riocli/auth/status.py @@ -18,7 +18,7 @@ @click.command( - 'status', + "status", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, @@ -27,8 +27,8 @@ def status(ctx: click.Context): """Shows the current login status.""" if not ctx.obj.exists: - click.secho('🔒You are logged out', fg=Colors.YELLOW) + click.secho("🔒You are logged out", fg=Colors.YELLOW) raise SystemExit(1) - if 'auth_token' in ctx.obj.data: - click.secho('🎉 You are logged in', fg=Colors.GREEN) + if "auth_token" in ctx.obj.data: + click.secho("🎉 You are logged in", fg=Colors.GREEN) diff --git a/riocli/auth/token.py b/riocli/auth/token.py index def9fe65..525aeffc 100644 --- a/riocli/auth/token.py +++ b/riocli/auth/token.py @@ -21,16 +21,19 @@ @click.command( - 'token', + "token", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) @click.option("--email", default=None, help="Email of the Rapyuta.io account") -@click.option("--password", default=None, hide_input=True, - help="Password for the Rapyuta.io account") -@click.option("--level", default=0, - help="Level of the token. 0 = low, 1 = med, 2 = high") +@click.option( + "--password", + default=None, + hide_input=True, + help="Password for the Rapyuta.io account", +) +@click.option("--level", default=0, help="Level of the token. 0 = low, 1 = med, 2 = high") def token(email: str, password: str, level: int = 0): """Generates a new rapyuta.io auth token. @@ -48,14 +51,15 @@ def token(email: str, password: str, level: int = 0): if level not in TOKEN_LEVELS: click.secho( - 'Invalid token level. Valid levels are {0}'.format( - list(TOKEN_LEVELS.keys())), fg=Colors.RED) + "Invalid token level. Valid levels are {0}".format(list(TOKEN_LEVELS.keys())), + fg=Colors.RED, + ) raise SystemExit(1) if not email: email = config.data.get("email_id", None) - password = password or click.prompt('Password', hide_input=True) + password = password or click.prompt("Password", hide_input=True) if not config.exists or not email or not password: raise LoggedOut diff --git a/riocli/auth/util.py b/riocli/auth/util.py index 9c1e9736..2c8fef99 100644 --- a/riocli/auth/util.py +++ b/riocli/auth/util.py @@ -11,34 +11,33 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import os import json +import os import click +from munch import munchify from rapyuta_io import Client from rapyuta_io.clients.rip_client import AuthTokenLevel from rapyuta_io.utils import UnauthorizedError from rapyuta_io.utils.rest_client import HttpMethod, RestClient -from munch import munchify - from riocli.config import Configuration from riocli.constants import Colors, Symbols -from riocli.project.util import find_project_guid, find_organization_guid, get_organization_name +from riocli.project.util import ( + find_project_guid, + find_organization_guid, + get_organization_name, +) from riocli.utils.selector import show_selection from riocli.utils.spinner import with_spinner from riocli.v2client.util import handle_server_errors -TOKEN_LEVELS = { - 0: AuthTokenLevel.LOW, - 1: AuthTokenLevel.MED, - 2: AuthTokenLevel.HIGH -} +TOKEN_LEVELS = {0: AuthTokenLevel.LOW, 1: AuthTokenLevel.MED, 2: AuthTokenLevel.HIGH} def select_organization( - config: Configuration, - organization: str = None, + config: Configuration, + organization: str = None, ) -> str: """ Launches the org selection prompt by listing all the orgs that the user is a part of. @@ -50,16 +49,16 @@ def select_organization( org_guid, org_name, short_id = None, None, None if organization: - if organization.startswith('org-'): + if organization.startswith("org-"): org_guid = organization org_name, short_id = get_organization_name(client, org_guid) else: org_guid, short_id = find_organization_guid(client, name=organization) if org_guid and org_name and short_id: - config.data['organization_id'] = org_guid - config.data['organization_name'] = org_name - config.data['organization_short_id'] = short_id + config.data["organization_id"] = org_guid + config.data["organization_name"] = org_name + config.data["organization_short_id"] = short_id return org_guid # fetch user organizations and sort them on their name @@ -67,7 +66,9 @@ def select_organization( organizations = sorted(organizations, key=lambda org: org.name.lower()) if len(organizations) == 0: - click.secho("You are not a part of any organization", fg=Colors.BLACK, bg=Colors.WHITE) + click.secho( + "You are not a part of any organization", fg=Colors.BLACK, bg=Colors.WHITE + ) raise SystemExit(1) org_map, org_short_guids = {}, {} @@ -80,20 +81,20 @@ def select_organization( org_guid = show_selection(org_map, "Select an organization:") if org_guid and org_guid not in org_map: - click.secho('invalid organization guid', fg=Colors.RED) + click.secho("invalid organization guid", fg=Colors.RED) raise SystemExit(1) - config.data['organization_id'] = org_guid - config.data['organization_name'] = org_map[org_guid] - config.data['organization_short_id'] = org_short_guids[org_guid] + config.data["organization_id"] = org_guid + config.data["organization_name"] = org_map[org_guid] + config.data["organization_short_id"] = org_short_guids[org_guid] return org_guid def select_project( - config: Configuration, - project: str = None, - organization: str = None, + config: Configuration, + project: str = None, + organization: str = None, ) -> None: """ Launches the project selection prompt by listing all the projects. @@ -104,16 +105,21 @@ def select_project( project_guid = None if project: - project_guid = (project if project.startswith('project-') else - find_project_guid(client, project, - organization=organization)) + project_guid = ( + project + if project.startswith("project-") + else find_project_guid(client, project, organization=organization) + ) projects = client.list_projects(organization_guid=organization) if len(projects) == 0: - config.data['project_id'] = "" - config.data['project_name'] = "" - click.secho("There are no projects in this organization", - fg=Colors.BLACK, bg=Colors.WHITE) + config.data["project_id"] = "" + config.data["project_name"] = "" + click.secho( + "There are no projects in this organization", + fg=Colors.BLACK, + bg=Colors.WHITE, + ) return # Sort projects based on their names for an easier selection @@ -130,38 +136,41 @@ def select_project( if not project_guid: project_guid = show_selection( - project_map, header='Select the project to activate') + project_map, header="Select the project to activate" + ) - config.data['project_id'] = project_guid - config.data['project_name'] = project_map[project_guid] + config.data["project_id"] = project_guid + config.data["project_name"] = project_map[project_guid] confirmation = "Your project has been set to '{}' in the organization '{}'".format( - config.data['project_name'], config.data['organization_name'], + config.data["project_name"], + config.data["organization_name"], ) click.secho(confirmation, fg=Colors.GREEN) -@with_spinner(text='Fetching token...') +@with_spinner(text="Fetching token...") def get_token( - email: str, - password: str, - level: int = 1, - spinner=None, + email: str, + password: str, + level: int = 1, + spinner=None, ) -> str: """ Generates a new token using email and password. """ config = Configuration() - if 'environment' in config.data: - os.environ['RIO_CONFIG'] = config.filepath + if "environment" in config.data: + os.environ["RIO_CONFIG"] = config.filepath try: - token = Client.get_auth_token( - email, password, TOKEN_LEVELS[level]) + token = Client.get_auth_token(email, password, TOKEN_LEVELS[level]) return token except UnauthorizedError as e: - spinner.text = click.style("Incorrect email/password. Login again with `rio auth login`", fg=Colors.RED) + spinner.text = click.style( + "Incorrect email/password. Login again with `rio auth login`", fg=Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e except Exception as e: @@ -171,44 +180,44 @@ def get_token( def api_refresh_token( - token: str, + token: str, ) -> str: """ Refreshes the existing token using the Refresh Token API. """ config = Configuration() client = config.new_client(with_project=False) - rip_host = client._get_api_endpoints('rip_host') - url = '{}/refreshtoken'.format(rip_host) + rip_host = client._get_api_endpoints("rip_host") + url = "{}/refreshtoken".format(rip_host) - response = RestClient(url).method(HttpMethod.POST).execute(payload={'token': token}) + response = RestClient(url).method(HttpMethod.POST).execute(payload={"token": token}) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - return '' + return "" data = munchify(data) return data.data.Token -@with_spinner(text='Validating token...') +@with_spinner(text="Validating token...") def validate_and_set_token(ctx: click.Context, token: str, spinner=None) -> bool: """Validates an auth token.""" - if 'environment' in ctx.obj.data: - os.environ['RIO_CONFIG'] = ctx.obj.filepath + if "environment" in ctx.obj.data: + os.environ["RIO_CONFIG"] = ctx.obj.filepath client = Client(auth_token=token) try: user = client.get_authenticated_user() spinner.text = click.style( - 'Token belongs to user {}'.format(user.email_id), - fg=Colors.CYAN) + "Token belongs to user {}".format(user.email_id), fg=Colors.CYAN + ) # Save the token and user email_id in the context - ctx.obj.data['auth_token'] = token - ctx.obj.data['email_id'] = user.email_id + ctx.obj.data["auth_token"] = token + ctx.obj.data["email_id"] = user.email_id spinner.ok(Symbols.INFO) return True except UnauthorizedError: diff --git a/riocli/bootstrap.py b/riocli/bootstrap.py index 1d8de926..a2d91cad 100644 --- a/riocli/bootstrap.py +++ b/riocli/bootstrap.py @@ -24,7 +24,9 @@ from click_plugins import with_plugins from pkg_resources import iter_entry_points -from riocli.apply import apply, delete, explain, list_examples, template +from riocli.apply import apply, delete +from riocli.apply.explain import list_examples, explain +from riocli.apply.template import template from riocli.auth import auth from riocli.chart import chart from riocli.completion import completion @@ -47,11 +49,16 @@ from riocli.shell import deprecated_repl, shell from riocli.static_route import static_route from riocli.usergroup import usergroup -from riocli.utils import (check_for_updates, is_pip_installation, pip_install_cli, update_appimage) +from riocli.utils import ( + check_for_updates, + is_pip_installation, + pip_install_cli, + update_appimage, +) from riocli.vpn import vpn -@with_plugins(iter_entry_points('riocli.plugins')) +@with_plugins(iter_entry_points("riocli.plugins")) @click.group( invoke_without_command=False, cls=HelpColorsGroup, @@ -85,13 +92,20 @@ def cli_help(ctx): @cli.command() def version(): """View installed CLI and SDK versions.""" - click.echo(f'rio {__version__} / SDK {rapyuta_io.__version__}') - - -@cli.command('update') -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, - type=click.BOOL, default=False, - help="Skip confirmation") + click.echo(f"rio {__version__} / SDK {rapyuta_io.__version__}") + + +@cli.command("update") +@click.option( + "-f", + "--force", + "--silent", + "silent", + is_flag=True, + type=click.BOOL, + default=False, + help="Skip confirmation", +) def update(silent: bool) -> None: """Update the CLI to the latest version. @@ -105,14 +119,13 @@ def update(silent: bool) -> None: """ available, latest = check_for_updates(__version__) if not available: - click.secho('🎉 You are using the latest version', fg=Colors.GREEN) + click.secho("🎉 You are using the latest version", fg=Colors.GREEN) return - click.secho('🎉 A newer version ({}) is available.'.format(latest), - fg=Colors.GREEN) + click.secho("🎉 A newer version ({}) is available.".format(latest), fg=Colors.GREEN) if not silent: - click.confirm('Do you want to update?', abort=True, default=False) + click.confirm("Do you want to update?", abort=True, default=False) try: if is_pip_installation(): @@ -120,11 +133,10 @@ def update(silent: bool) -> None: else: update_appimage(version=latest) except Exception as e: - click.secho('{} Failed to update: {}'.format(Symbols.ERROR, e), fg=Colors.RED) + click.secho("{} Failed to update: {}".format(Symbols.ERROR, e), fg=Colors.RED) raise SystemExit(1) from e - click.secho('{} Update successful!'.format(Symbols.SUCCESS), - fg=Colors.GREEN) + click.secho("{} Update successful!".format(Symbols.SUCCESS), fg=Colors.GREEN) cli.add_command(apply) diff --git a/riocli/chart/apply.py b/riocli/chart/apply.py index c1f08f5c..d570cf0b 100644 --- a/riocli/chart/apply.py +++ b/riocli/chart/apply.py @@ -21,41 +21,76 @@ @click.command( - 'apply', + "apply", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, - help='Apply a new Rapyuta Chart in the Project', + help="Apply a new Rapyuta Chart in the Project", ) -@click.option('--dryrun', '-d', is_flag=True, default=False, - help='dry run the yaml files without applying any change') -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, - type=click.BOOL, default=False, - help="Skip confirmation") -@click.option('--values', '-v', multiple=True, default=(), - help="Path to values yaml file. key/values specified in the " - "values file can be used as variables in template yamls") -@click.option('--secrets', '-s', multiple=True, default=(), - help="Secret files are sops encoded value files. rio-cli " - "expects sops to be authorized for decoding files on " - "this computer") -@click.option('--workers', '-w', - help="number of parallel workers while running apply command. " - "defaults to 6.") -@click.option('--retry-count', '-rc', type=int, default=50, - help="Number of retries before a resource creation times out status, defaults to 50") -@click.option('--retry-interval', '-ri', type=int, default=6, - help="Interval between retries defaults to 6") -@click.argument('chart', type=str) +@click.option( + "--dryrun", + "-d", + is_flag=True, + default=False, + help="dry run the yaml files without applying any change", +) +@click.option( + "-f", + "--force", + "--silent", + "silent", + is_flag=True, + type=click.BOOL, + default=False, + help="Skip confirmation", +) +@click.option( + "--values", + "-v", + multiple=True, + default=(), + help="Path to values yaml file. key/values specified in the " + "values file can be used as variables in template yamls", +) +@click.option( + "--secrets", + "-s", + multiple=True, + default=(), + help="Secret files are sops encoded value files. rio-cli " + "expects sops to be authorized for decoding files on " + "this computer", +) +@click.option( + "--workers", + "-w", + help="number of parallel workers while running apply command. " "defaults to 6.", +) +@click.option( + "--retry-count", + "-rc", + type=int, + default=50, + help="Number of retries before a resource creation times out status, defaults to 50", +) +@click.option( + "--retry-interval", + "-ri", + type=int, + default=6, + help="Interval between retries defaults to 6", +) +@click.argument("chart", type=str) def apply_chart( - chart: str, - values: str, - secrets: str, - dryrun: bool, - workers: int = 6, - retry_count: int = 50, - retry_interval: int = 6, - silent: bool = False) -> None: + chart: str, + values: str, + secrets: str, + dryrun: bool, + workers: int = 6, + retry_count: int = 50, + retry_interval: int = 6, + silent: bool = False, +) -> None: """Install a chart from the rapyuta-charts repository. This command is based on the ``rio apply`` command. However, @@ -87,10 +122,18 @@ def apply_chart( versions = find_chart(chart) if len(versions) > 1: click.secho( - 'More than one charts are available, please specify the version!', - fg=Colors.RED) + "More than one charts are available, please specify the version!", + fg=Colors.RED, + ) chart = Chart(**versions[0]) - chart.apply_chart(values, secrets, dryrun=dryrun, workers=workers, - silent=silent, retry_count=retry_count, retry_interval=retry_interval) + chart.apply_chart( + values, + secrets, + dryrun=dryrun, + workers=workers, + silent=silent, + retry_count=retry_count, + retry_interval=retry_interval, + ) chart.cleanup() diff --git a/riocli/chart/chart.py b/riocli/chart/chart.py index 16f63f9d..3874c16a 100644 --- a/riocli/chart/chart.py +++ b/riocli/chart/chart.py @@ -30,48 +30,65 @@ def __init__(self, *args, **kwargs): self.downloaded = False def apply_chart( - self, - values: str = None, - secrets: str = None, - dryrun: bool = None, - workers: int = 6, - retry_count: int = 50, - retry_interval: int = 6, - silent: bool = False): + self, + values: str = None, + secrets: str = None, + dryrun: bool = None, + workers: int = 6, + retry_count: int = 50, + retry_interval: int = 6, + silent: bool = False, + ): if not self.downloaded: self.download_chart() - templates_dir = Path(self.tmp_dir.name, self.name, 'templates') + templates_dir = Path(self.tmp_dir.name, self.name, "templates") if not values: - values = Path(self.tmp_dir.name, self.name, - 'values.yaml').as_posix() - - apply.callback(values=values, files=[templates_dir], secrets=secrets, - dryrun=dryrun, workers=workers, silent=silent, retry_count=retry_count, retry_interval=retry_interval) + values = Path(self.tmp_dir.name, self.name, "values.yaml").as_posix() + + apply.callback( + values=values, + files=[templates_dir], + secrets=secrets, + dryrun=dryrun, + workers=workers, + silent=silent, + retry_count=retry_count, + retry_interval=retry_interval, + ) def delete_chart( - self, - values: str = None, - secrets: str = None, - dryrun: bool = None, - silent: bool = False): + self, + values: str = None, + secrets: str = None, + dryrun: bool = None, + silent: bool = False, + ): if not self.downloaded: self.download_chart() - templates_dir = Path(self.tmp_dir.name, self.name, 'templates') + templates_dir = Path(self.tmp_dir.name, self.name, "templates") if not values: - values = Path(self.tmp_dir.name, self.name, - 'values.yaml').as_posix() + values = Path(self.tmp_dir.name, self.name, "values.yaml").as_posix() - delete.callback(values=values, files=[templates_dir], secrets=secrets, - dryrun=dryrun, silent=silent) + delete.callback( + values=values, + files=[templates_dir], + secrets=secrets, + dryrun=dryrun, + silent=silent, + ) def download_chart(self): self._create_temp_directory() - click.secho('Downloading {}:{} chart in {}'.format( - self.name, self.version, self.tmp_dir.name), fg=Colors.CYAN) + click.secho( + "Downloading {}:{} chart in {}".format( + self.name, self.version, self.tmp_dir.name + ), + fg=Colors.CYAN, + ) chart_filepath = Path(self.tmp_dir.name, self._chart_filename()) - with open(chart_filepath, 'wb') as f: + with open(chart_filepath, "wb") as f: resp = requests.get(self.urls[0]) f.write(resp.content) @@ -91,8 +108,8 @@ def cleanup(self): self.tmp_dir.cleanup() def _chart_filename(self): - return self.urls[0].split('/')[-1] + return self.urls[0].split("/")[-1] def _create_temp_directory(self): - prefix = 'rio-chart-{}-'.format(self.name) + prefix = "rio-chart-{}-".format(self.name) self.tmp_dir = TemporaryDirectory(prefix=prefix) diff --git a/riocli/chart/delete.py b/riocli/chart/delete.py index 9e21a8b2..93011b26 100644 --- a/riocli/chart/delete.py +++ b/riocli/chart/delete.py @@ -20,30 +20,54 @@ @click.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, - help='Delete the Rapyuta Chart from the Project', + help="Delete the Rapyuta Chart from the Project", ) -@click.option('--dryrun', '-d', is_flag=True, default=False, - help='Dry run the yaml files without applying any change') -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, - type=click.BOOL, default=False, help="Skip confirmation") -@click.option('--values', '-v', multiple=True, default=(), - help=("Path to values yaml file. key/values specified in the" - "values file can be used as variables in template yamls")) -@click.option('--secrets', '-s', multiple=True, default=(), - help=("Secret files are sops encoded value files. rio-cli " - "expects sops to be authorized for decoding files on " - "this computer")) -@click.argument('chart', type=str) +@click.option( + "--dryrun", + "-d", + is_flag=True, + default=False, + help="Dry run the yaml files without applying any change", +) +@click.option( + "-f", + "--force", + "--silent", + "silent", + is_flag=True, + type=click.BOOL, + default=False, + help="Skip confirmation", +) +@click.option( + "--values", + "-v", + multiple=True, + default=(), + help=( + "Path to values yaml file. key/values specified in the" + "values file can be used as variables in template yamls" + ), +) +@click.option( + "--secrets", + "-s", + multiple=True, + default=(), + help=( + "Secret files are sops encoded value files. rio-cli " + "expects sops to be authorized for decoding files on " + "this computer" + ), +) +@click.argument("chart", type=str) def delete_chart( - chart: str, - values: str, - secrets: str, - dryrun: bool = False, - silent: bool = False) -> None: + chart: str, values: str, secrets: str, dryrun: bool = False, silent: bool = False +) -> None: """Delete a chart. The delete command is based on the `rio delete` command @@ -74,10 +98,11 @@ def delete_chart( """ versions = find_chart(chart) if len(versions) > 1: - click.secho('More than one charts are available, ' - 'please specify the version!', fg=Colors.YELLOW) + click.secho( + "More than one charts are available, " "please specify the version!", + fg=Colors.YELLOW, + ) chart = Chart(**versions[0]) - chart.delete_chart(values=values, secrets=secrets, - dryrun=dryrun, silent=silent) + chart.delete_chart(values=values, secrets=secrets, dryrun=dryrun, silent=silent) chart.cleanup() diff --git a/riocli/chart/info.py b/riocli/chart/info.py index cc9bda71..09cae3ca 100644 --- a/riocli/chart/info.py +++ b/riocli/chart/info.py @@ -21,13 +21,13 @@ @click.command( - 'info', + "info", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, - help='Describe the available chart with versions', + help="Describe the available chart with versions", ) -@click.argument('chart', type=str) +@click.argument("chart", type=str) def info_chart(chart: str) -> None: """Print a chart's details.""" versions = find_chart(chart) diff --git a/riocli/chart/list.py b/riocli/chart/list.py index c4339440..c5d2dac5 100644 --- a/riocli/chart/list.py +++ b/riocli/chart/list.py @@ -21,20 +21,19 @@ @click.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('-w', '--wide', is_flag=True, default=False, - help='Print more details') +@click.option("-w", "--wide", is_flag=True, default=False, help="Print more details") def list_charts(wide: bool = False) -> None: """List all available charts.""" index = fetch_index() - if 'entries' not in index: - raise Exception('No entries found!') + if "entries" not in index: + raise Exception("No entries found!") entries = [] - for name, chart in index['entries'].items(): + for _, chart in index["entries"].items(): for version in chart: entries.append(version) diff --git a/riocli/chart/search.py b/riocli/chart/search.py index 0e85d19f..84d9188c 100644 --- a/riocli/chart/search.py +++ b/riocli/chart/search.py @@ -22,18 +22,16 @@ @click.command( - 'search', + "search", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, - help='Search for available charts in the repository', + help="Search for available charts in the repository", ) -@click.option('-w', '--wide', is_flag=True, default=False, - help='Print more details') -@click.argument('chart', type=str) +@click.option("-w", "--wide", is_flag=True, default=False, help="Print more details") +@click.argument("chart", type=str) @with_spinner(text="Searching for chart...") -def search_chart(chart: str, wide: bool = False, - spinner: Yaspin = None) -> None: +def search_chart(chart: str, wide: bool = False, spinner: Yaspin = None) -> None: """Search for a chart in the chart repo.""" try: versions = find_chart(chart) diff --git a/riocli/chart/util.py b/riocli/chart/util.py index 2c4476f9..249b94ac 100644 --- a/riocli/chart/util.py +++ b/riocli/chart/util.py @@ -5,7 +5,9 @@ from riocli.utils import tabulate_data -DEFAULT_REPOSITORY = 'https://rapyuta-robotics.github.io/rapyuta-charts/incubator/index.yaml' # noqa +DEFAULT_REPOSITORY = ( + "https://rapyuta-robotics.github.io/rapyuta-charts/incubator/index.yaml" # noqa +) def find_chart(chart: str) -> typing.List: @@ -13,13 +15,13 @@ def find_chart(chart: str) -> typing.List: chart, ver = parse_chart(chart) index = fetch_index() - if 'entries' not in index: - raise Exception('No entries found!') + if "entries" not in index: + raise Exception("No entries found!") - if chart not in index['entries']: - raise Exception('No such chart found!') + if chart not in index["entries"]: + raise Exception("No such chart found!") - versions = index['entries'][chart] + versions = index["entries"][chart] if ver: versions = _find_version(entries=versions, version=ver) @@ -30,7 +32,7 @@ def fetch_index(repository=DEFAULT_REPOSITORY) -> typing.Dict: """Fetches the upstream chart index.""" response = requests.get(repository) if not response.ok: - raise Exception('Fetching index failed: %s'.format(repository)) + raise Exception(f"Fetching index failed: {repository}") index = safe_load(response.text) return index @@ -50,9 +52,9 @@ def parse_chart(val: str) -> (str, str): # chart = splits[0] # Separate version - splits = val.split(':') + splits = val.split(":") if len(splits) > 2: - raise Exception('Multiple : are not allowed in the chart!') + raise Exception("Multiple : are not allowed in the chart!") elif len(splits) == 2: chart, ver = splits[0], splits[1] else: @@ -63,23 +65,23 @@ def parse_chart(val: str) -> (str, str): def _find_version(entries: typing.List, version: str): for entry in entries: - if entry.get('version') == version: + if entry.get("version") == version: return [entry] def print_chart_entries(entries: typing.List, wide: bool = False) -> None: """Prints charts in a tabular format.""" - entries = sorted(entries, key=lambda x: x.get('name').lower()) + entries = sorted(entries, key=lambda x: x.get("name").lower()) - headers = ['Name', 'Version', 'Created At'] + headers = ["Name", "Version", "Created At"] if wide: - headers.append('Description') + headers.append("Description") data = [] for entry in entries: - row = [entry.get('name'), entry.get('version'), entry.get('created')] + row = [entry.get("name"), entry.get("version"), entry.get("created")] if wide: - row.append(entry.get('description')) + row.append(entry.get("description")) data.append(row) diff --git a/riocli/completion/__init__.py b/riocli/completion/__init__.py index 56c693ed..99cdb9d3 100644 --- a/riocli/completion/__init__.py +++ b/riocli/completion/__init__.py @@ -19,10 +19,10 @@ @click.command( cls=HelpColorsCommand, - help_headers_color='yellow', - help_options_color='green', + help_headers_color="yellow", + help_options_color="green", ) -@click.argument('shell-name', type=click.Choice(['zsh', 'fish', 'bash'])) +@click.argument("shell-name", type=click.Choice(["zsh", "fish", "bash"])) def completion(shell_name): """ Output shell completion code for the specified shell @@ -35,5 +35,5 @@ def completion(shell_name): For Fish: $ eval (command rio completion bash) """ - os.environ['_RIO_COMPLETE'] = '{}_source'.format(shell_name) - os.system('rio') + os.environ["_RIO_COMPLETE"] = "{}_source".format(shell_name) + os.system("rio") diff --git a/riocli/config/config.py b/riocli/config/config.py index 0560c290..5f1aac43 100644 --- a/riocli/config/config.py +++ b/riocli/config/config.py @@ -23,7 +23,12 @@ from click import get_app_dir from rapyuta_io import Client -from riocli.exceptions import LoggedOut, NoOrganizationSelected, NoProjectSelected, HwilLoggedOut +from riocli.exceptions import ( + LoggedOut, + NoOrganizationSelected, + NoProjectSelected, + HwilLoggedOut, +) from riocli.hwilclient import Client as HwilClient from riocli.v2client import Client as v2Client @@ -42,10 +47,13 @@ class Configuration(object): "project_id": "" } """ - APP_NAME = 'rio-cli' - PIPING_SERVER = 'https://piping-server-v0-rapyuta-infra.apps.okd4v2.okd4beta.rapyuta.io' - DIFF_TOOL = 'diff' - MERGE_TOOL = 'vimdiff' + + APP_NAME = "rio-cli" + PIPING_SERVER = ( + "https://piping-server-v0-rapyuta-infra.apps.okd4v2.okd4beta.rapyuta.io" + ) + DIFF_TOOL = "diff" + MERGE_TOOL = "vimdiff" def __init__(self, filepath: Optional[str] = None): self._filepath = filepath @@ -57,7 +65,7 @@ def __init__(self, filepath: Optional[str] = None): self.data = dict() return - with open(self.filepath, 'r') as config_file: + with open(self.filepath, "r") as config_file: self.data = json.load(config_file) def save(self: Configuration): @@ -68,23 +76,23 @@ def save(self: Configuration): if exc.errno != errno.EEXIST: raise - with open(self.filepath, 'w') as config_file: + with open(self.filepath, "w") as config_file: json.dump(self.data, config_file) # We are using lru_cache to cache the calls to new_v2_client # with project and without project. This is to avoid creating a # new client object every time we call new_v2_client. # https://docs.python.org/3.8/library/functools.html#functools.lru_cache - @lru_cache(maxsize=2) + @lru_cache(maxsize=2) # noqa: B019 def new_client(self: Configuration, with_project: bool = True) -> Client: - if 'auth_token' not in self.data: + if "auth_token" not in self.data: raise LoggedOut - if 'environment' in self.data: - os.environ['RIO_CONFIG'] = self.filepath + if "environment" in self.data: + os.environ["RIO_CONFIG"] = self.filepath - token = self.data.get('auth_token', None) - project = self.data.get('project_id', None) + token = self.data.get("auth_token", None) + project = self.data.get("project_id", None) if with_project and project is None: raise NoProjectSelected @@ -93,16 +101,16 @@ def new_client(self: Configuration, with_project: bool = True) -> Client: return Client(auth_token=token, project=project) - @lru_cache(maxsize=2) + @lru_cache(maxsize=2) # noqa: B019 def new_v2_client(self: Configuration, with_project: bool = True) -> v2Client: - if 'auth_token' not in self.data: + if "auth_token" not in self.data: raise LoggedOut - if 'environment' in self.data: - os.environ['RIO_CONFIG'] = self.filepath + if "environment" in self.data: + os.environ["RIO_CONFIG"] = self.filepath - token = self.data.get('auth_token', None) - project = self.data.get('project_id', None) + token = self.data.get("auth_token", None) + project = self.data.get("project_id", None) if with_project and project is None: raise NoProjectSelected @@ -112,23 +120,23 @@ def new_v2_client(self: Configuration, with_project: bool = True) -> v2Client: return v2Client(self, auth_token=token, project=project) def new_hwil_client(self: Configuration) -> HwilClient: - if 'hwil_auth_token' not in self.data: + if "hwil_auth_token" not in self.data: raise HwilLoggedOut - if 'environment' in self.data: - os.environ['RIO_CONFIG'] = self.filepath + if "environment" in self.data: + os.environ["RIO_CONFIG"] = self.filepath - token = self.data.get('hwil_auth_token', None) + token = self.data.get("hwil_auth_token", None) return HwilClient(auth_token=token) def get_auth_header(self: Configuration) -> dict: - if not ('auth_token' in self.data and 'project_id' in self.data): + if not ("auth_token" in self.data and "project_id" in self.data): raise LoggedOut - token, project = self.data['auth_token'], self.data['project_id'] - if not token.startswith('Bearer'): - token = 'Bearer {}'.format(token) + token, project = self.data["auth_token"], self.data["project_id"] + if not token.startswith("Bearer"): + token = "Bearer {}".format(token) return dict(Authorization=token, project=project) @@ -141,13 +149,13 @@ def filepath(self: Configuration) -> str: @property def project_guid(self: Configuration) -> str: - if 'auth_token' not in self.data: + if "auth_token" not in self.data: raise LoggedOut - if 'organization_id' not in self.data: + if "organization_id" not in self.data: raise NoOrganizationSelected - guid = self.data.get('project_id') + guid = self.data.get("project_id") if guid is None: raise NoProjectSelected @@ -155,10 +163,10 @@ def project_guid(self: Configuration) -> str: @property def organization_guid(self: Configuration) -> str: - if 'auth_token' not in self.data: + if "auth_token" not in self.data: raise LoggedOut - guid = self.data.get('organization_id') + guid = self.data.get("organization_id") if guid is None: raise NoOrganizationSelected @@ -166,10 +174,10 @@ def organization_guid(self: Configuration) -> str: @property def organization_short_id(self: Configuration) -> str: - if 'auth_token' not in self.data: + if "auth_token" not in self.data: raise LoggedOut - short_id = self.data.get('organization_short_id') + short_id = self.data.get("organization_short_id") if short_id is None: raise NoOrganizationSelected @@ -177,20 +185,20 @@ def organization_short_id(self: Configuration) -> str: @property def piping_server(self: Configuration): - return self.data.get('piping_server', self.PIPING_SERVER) + return self.data.get("piping_server", self.PIPING_SERVER) @property def diff_tool(self: Configuration): - return self.data.get('diff_tool', self.DIFF_TOOL) + return self.data.get("diff_tool", self.DIFF_TOOL) @property def merge_tool(self: Configuration): - return self.data.get('merge_tool', self.MERGE_TOOL) + return self.data.get("merge_tool", self.MERGE_TOOL) @property def machine_id(self: Configuration): - if 'machine_id' not in self.data: - self.data['machine_id'] = str(uuid.uuid4()) + if "machine_id" not in self.data: + self.data["machine_id"] = str(uuid.uuid4()) self.save() - return self.data.get('machine_id') + return self.data.get("machine_id") diff --git a/riocli/config/context.py b/riocli/config/context.py index 95add3f5..d5681137 100644 --- a/riocli/config/context.py +++ b/riocli/config/context.py @@ -25,7 +25,7 @@ @click.group( - name='context', + name="context", invoke_without_command=False, cls=HelpColorsGroup, help_headers_color=Colors.YELLOW, @@ -42,13 +42,18 @@ def cli_context() -> None: @cli_context.command( - name='view', + name="view", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--format', '-f', 'format_type', default='yaml', - type=click.Choice(['json', 'yaml'], case_sensitive=False)) +@click.option( + "--format", + "-f", + "format_type", + default="yaml", + type=click.Choice(["json", "yaml"], case_sensitive=False), +) def view_cli_context(format_type: Optional[str]) -> None: """View the current CLI context. @@ -61,27 +66,32 @@ def view_cli_context(format_type: Optional[str]) -> None: @cli_context.command( hidden=True, - name='set', + name="set", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('file', type=click.Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True, path_type=Path)) +@click.argument( + "file", + type=click.Path( + exists=True, file_okay=True, dir_okay=False, resolve_path=True, path_type=Path + ), +) def set_cli_context(file: Path) -> None: """Set the CLI context. This command sets the CLI context to the values in the specified file. The supported file formats are JSON and YAML only. """ - with file.open(mode='r') as fp: - if file.suffix == '.json': + with file.open(mode="r") as fp: + if file.suffix == ".json": data = json.load(fp) - elif file.suffix == '.yaml': + elif file.suffix == ".yaml": data = yaml.safe_load(fp) else: - raise Exception('unsupported file format') + raise Exception("unsupported file format") Configuration().data = data Configuration().save() - click.secho(f'{Symbols.SUCCESS} Context has been updated.', fg=Colors.GREEN) + click.secho(f"{Symbols.SUCCESS} Context has been updated.", fg=Colors.GREEN) diff --git a/riocli/configtree/__init__.py b/riocli/configtree/__init__.py index 53e2935b..be3d3253 100644 --- a/riocli/configtree/__init__.py +++ b/riocli/configtree/__init__.py @@ -16,14 +16,23 @@ from riocli.configtree.diff import diff_revisions from riocli.configtree.export_keys import export_keys +from riocli.configtree.import_keys import import_keys from riocli.configtree.merge import merge_revisions from riocli.configtree.revision import revision -from riocli.configtree.tree import clone_tree, create_config_tree, delete_config_tree, list_config_tree_keys, list_config_trees, list_tree_revisions, set_tree_revision -from riocli.configtree.import_keys import import_keys +from riocli.configtree.tree import ( + clone_tree, + create_config_tree, + delete_config_tree, + list_config_tree_keys, + list_config_trees, + list_tree_revisions, + set_tree_revision, +) from riocli.constants.colors import Colors + @click.group( - name='configtree', + name="configtree", invoke_without_command=False, cls=HelpColorsGroup, help_headers_color=Colors.YELLOW, diff --git a/riocli/configtree/diff.py b/riocli/configtree/diff.py index ccf46f81..915b5f85 100644 --- a/riocli/configtree/diff.py +++ b/riocli/configtree/diff.py @@ -13,6 +13,7 @@ # limitations under the License. import os from tempfile import NamedTemporaryFile + import click from click_help_colors import HelpColorsCommand @@ -20,14 +21,15 @@ from riocli.configtree.util import fetch_ref_keys, unflatten_keys from riocli.constants.colors import Colors + @click.command( - 'diff', + "diff", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('ref_1', type=str) -@click.argument('ref_2', type=str) +@click.argument("ref_1", type=str) +@click.argument("ref_2", type=str) @click.pass_context def diff_revisions(ctx: click.Context, ref_1: str, ref_2: str): """ @@ -35,7 +37,7 @@ def diff_revisions(ctx: click.Context, ref_1: str, ref_2: str): The ref is a slash ('/') separated string. - * The first part can be 'org' or 'proj' defining the scope of the reference. + * The first part can be 'org' or 'proj' defining the scope of the reference. * The second part defines the name of the Tree. @@ -43,7 +45,7 @@ def diff_revisions(ctx: click.Context, ref_1: str, ref_2: str): Examples: - * org/tree-name + * org/tree-name * org/tree-name/rev-id @@ -70,8 +72,9 @@ def display_diff(ctx: click.Context, keys_1: dict, keys_2: dict) -> None: keys_1 = unflatten_keys(keys_1) keys_2 = unflatten_keys(keys_2) - with NamedTemporaryFile(mode='w+b') as file_1, NamedTemporaryFile(mode='w+b') as file_2: + with NamedTemporaryFile(mode="w+b") as file_1, NamedTemporaryFile( + mode="w+b" + ) as file_2: keys_1.to_json(filepath=file_1.name, indent=4) keys_2.to_json(filepath=file_2.name, indent=4) - os.system('{} {} {}'.format(cfg.diff_tool, file_1.name, file_2.name)) - + os.system("{} {} {}".format(cfg.diff_tool, file_1.name, file_2.name)) diff --git a/riocli/configtree/etcd.py b/riocli/configtree/etcd.py index f4a503dd..7f0f92e9 100644 --- a/riocli/configtree/etcd.py +++ b/riocli/configtree/etcd.py @@ -20,17 +20,17 @@ def import_in_etcd( - data: dict, - endpoint: str, - port: Optional[int] = 2379, - prefix: Optional[str] = None, + data: dict, + endpoint: str, + port: Optional[int] = 2379, + prefix: Optional[str] = None, ) -> None: cli = Etcd3Client(host=endpoint, port=port) try: cli.status() - except Exception as e: - raise ConnectionError(f'cannot connect to etcd server at {endpoint}:{port}') + except Exception: + raise ConnectionError(f"cannot connect to etcd server at {endpoint}:{port}") if prefix: cli.delete_prefix(prefix) @@ -39,52 +39,53 @@ def import_in_etcd( compares, failures = [], [] - prefix = prefix or '' + prefix = prefix or "" for key, val in data.items(): - key = '{}/{}'.format(prefix, key) - - enc_key = b64encode(str(key).encode('utf-8')).decode() - enc_val = b64encode(str(val).encode('utf-8')).decode() - compares.append({ - 'key': enc_key, - 'result': 'EQUAL', - 'target': 'VALUE', - 'value': enc_val, - }) - failures.append({ - 'request_put': { - 'key': enc_key, - 'value': enc_val + key = "{}/{}".format(prefix, key) + + enc_key = b64encode(str(key).encode("utf-8")).decode() + enc_val = b64encode(str(val).encode("utf-8")).decode() + compares.append( + { + "key": enc_key, + "result": "EQUAL", + "target": "VALUE", + "value": enc_val, } - }) - - sentinel_key = b64encode('/sentinel_key'.encode('utf-8')).decode() - sentinel_val = b64encode(f'{time.time_ns()}|riocli-import'.encode('utf-8')).decode() - - compares.append({ - 'key': sentinel_key, - 'result': 'EQUAL', - 'target': 'VALUE', - 'value': sentinel_val, - }) - failures.append({ - 'request_put': { - 'key': sentinel_key, - 'value': sentinel_val, + ) + failures.append({"request_put": {"key": enc_key, "value": enc_val}}) + + sentinel_key = b64encode("/sentinel_key".encode("utf-8")).decode() + sentinel_val = b64encode(f"{time.time_ns()}|riocli-import".encode("utf-8")).decode() + + compares.append( + { + "key": sentinel_key, + "result": "EQUAL", + "target": "VALUE", + "value": sentinel_val, } - }) + ) + failures.append( + { + "request_put": { + "key": sentinel_key, + "value": sentinel_val, + } + } + ) txn = { - 'compare': compares, - 'failure': failures, + "compare": compares, + "failure": failures, } cli.transaction(txn) def _delete_all_keys(client: Etcd3Client) -> None: - null_char = '\x00' - enc_null = b64encode(null_char.encode('utf-8')).decode() + null_char = "\x00" + enc_null = b64encode(null_char.encode("utf-8")).decode() - client.delete('\x00', range_end=enc_null) + client.delete("\x00", range_end=enc_null) diff --git a/riocli/configtree/export_keys.py b/riocli/configtree/export_keys.py index d235a2e1..02bccb86 100644 --- a/riocli/configtree/export_keys.py +++ b/riocli/configtree/export_keys.py @@ -25,55 +25,73 @@ @click.command( - 'export', + "export", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') -@click.option('--export-directory', 'export_directory', type=str, - help='Path to the directory for exporting files.') -@click.option('--format', '-f', 'file_format', type=click.Choice(['json', 'yaml']), - default='json', help='Format of the exported files.') -@click.argument('tree-name', type=str) -@click.argument('rev-id', type=str, required=False) +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) +@click.option( + "--export-directory", + "export_directory", + type=str, + help="Path to the directory for exporting files.", +) +@click.option( + "--format", + "-f", + "file_format", + type=click.Choice(["json", "yaml"]), + default="json", + help="Format of the exported files.", +) +@click.argument("tree-name", type=str) +@click.argument("rev-id", type=str, required=False) @click.pass_context @with_spinner(text="Exporting keys...") def export_keys( - _: click.Context, - tree_name: str, - rev_id: Optional[str], - with_org: bool, - export_directory: Optional[str], - file_format: Optional[str], - spinner: Yaspin, + _: click.Context, + tree_name: str, + rev_id: Optional[str], + with_org: bool, + export_directory: Optional[str], + file_format: Optional[str], + spinner: Yaspin, ) -> None: """Export keys of the Config tree to files.""" if export_directory is None: - export_directory = '.' + export_directory = "." export_directory = Path(export_directory).absolute() try: client = new_v2_client(with_project=(not with_org)) - tree = client.get_config_tree(tree_name=tree_name, rev_id=rev_id, include_data=True) - if not tree.get('head'): - raise Exception('Config tree does not have keys in the revision') + tree = client.get_config_tree( + tree_name=tree_name, rev_id=rev_id, include_data=True + ) + if not tree.get("head"): + raise Exception("Config tree does not have keys in the revision") - keys = tree.get('keys') + keys = tree.get("keys") if not isinstance(keys, dict): - raise Exception('Keys are not a dictionary') + raise Exception("Keys are not a dictionary") data = unflatten_keys(keys) export_to_files(base_dir=export_directory, data=data, file_format=file_format) - spinner.text = click.style(f'Keys exported to {export_directory}', fg=Colors.GREEN) + spinner.text = click.style( + f"Keys exported to {export_directory}", fg=Colors.GREEN + ) spinner.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style(f'Failed to export keys: {e}', fg=Colors.RED) + spinner.text = click.style(f"Failed to export keys: {e}", fg=Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e - - diff --git a/riocli/configtree/import_keys.py b/riocli/configtree/import_keys.py index 56a3fee4..6233d00c 100644 --- a/riocli/configtree/import_keys.py +++ b/riocli/configtree/import_keys.py @@ -28,50 +28,99 @@ @click.command( - 'import', + "import", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--commit/--no-commit', 'commit', is_flag=True, type=bool, - help='Commit the imported keys to the Config Tree.') -@click.option('--update-head/--no-update-head', 'update_head', is_flag=True, type=bool, - help='Update the HEAD of the Config Tree after importing the keys.') -@click.option('--milestone', 'milestone', type=str, - help='Milestone name for the imported revision.') -@click.option('--etcd-endpoint', 'etcd_endpoint', type=str, - help='Import keys to local etcd instead of rapyuta.io cloud') -@click.option('--export-directory', 'export_directory', type=str, - help='Path to the directory for exporting files.') -@click.option('--export-format', 'export_format', type=click.Choice(['json', 'yaml']), - default='json', help='Format of the exported files.') -@click.option('--etcd-port', 'etcd_port', type=int, default=2379, - help='Port for the etcd endpoint') -@click.option('--etcd-prefix', 'etcd_prefix', type=str, - help='Prefix to use for the key-space') -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') -@click.option('--override', 'overrides', type=click.Path(exists=True), default=None, - multiple=True, help='Override values for keys in the imported files.') -@click.argument('tree-name', type=str) -@click.argument('files', type=click.Path(exists=True, file_okay=True, dir_okay=True, resolve_path=True), nargs=-1) +@click.option( + "--commit/--no-commit", + "commit", + is_flag=True, + type=bool, + help="Commit the imported keys to the Config Tree.", +) +@click.option( + "--update-head/--no-update-head", + "update_head", + is_flag=True, + type=bool, + help="Update the HEAD of the Config Tree after importing the keys.", +) +@click.option( + "--milestone", + "milestone", + type=str, + help="Milestone name for the imported revision.", +) +@click.option( + "--etcd-endpoint", + "etcd_endpoint", + type=str, + help="Import keys to local etcd instead of rapyuta.io cloud", +) +@click.option( + "--export-directory", + "export_directory", + type=str, + help="Path to the directory for exporting files.", +) +@click.option( + "--export-format", + "export_format", + type=click.Choice(["json", "yaml"]), + default="json", + help="Format of the exported files.", +) +@click.option( + "--etcd-port", + "etcd_port", + type=int, + default=2379, + help="Port for the etcd endpoint", +) +@click.option( + "--etcd-prefix", "etcd_prefix", type=str, help="Prefix to use for the key-space" +) +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) +@click.option( + "--override", + "overrides", + type=click.Path(exists=True), + default=None, + multiple=True, + help="Override values for keys in the imported files.", +) +@click.argument("tree-name", type=str) +@click.argument( + "files", + type=click.Path(exists=True, file_okay=True, dir_okay=True, resolve_path=True), + nargs=-1, +) @click.pass_context @with_spinner(text="Importing keys...") def import_keys( - _: click.Context, - tree_name: str, - files: Iterable[str], - commit: bool, - update_head: bool, - milestone: Optional[str], - export_directory: Optional[str], - export_format: Optional[str], - etcd_endpoint: Optional[str], - etcd_port: Optional[int], - etcd_prefix: Optional[str], - overrides: Optional[Iterable[str]], - with_org: bool, - spinner: Yaspin, + _: click.Context, + tree_name: str, + files: Iterable[str], + commit: bool, + update_head: bool, + milestone: Optional[str], + export_directory: Optional[str], + export_format: Optional[str], + etcd_endpoint: Optional[str], + etcd_port: Optional[int], + etcd_prefix: Optional[str], + overrides: Optional[Iterable[str]], + with_org: bool, + spinner: Yaspin, ) -> None: """Imports keys from JSON or YAML files. @@ -101,7 +150,7 @@ def import_keys( Note: If --etcd-endpoint is provided, the keys are imported to the local etcd cluster instead of the rapyuta.io cloud. """ if not files: - spinner.text = click.style('No files provided.', fg=Colors.RED) + spinner.text = click.style("No files provided.", fg=Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) @@ -109,33 +158,52 @@ def import_keys( if export_directory is not None: try: - export_to_files(base_dir=export_directory, data=data, file_format=export_format) - spinner.write(click.style( - f'{Symbols.SUCCESS} Keys exported to {export_format} files in {Path(export_directory).absolute()}.', - fg=Colors.GREEN)) + export_to_files( + base_dir=export_directory, data=data, file_format=export_format + ) + spinner.write( + click.style( + f"{Symbols.SUCCESS} Keys exported to {export_format} files in {Path(export_directory).absolute()}.", + fg=Colors.GREEN, + ) + ) except Exception as e: - spinner.text = click.style(f'Error exporting keys to files: {e}', fg=Colors.RED) + spinner.text = click.style( + f"Error exporting keys to files: {e}", fg=Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e - data = benedict(data).flatten(separator='/') - metadata = benedict(metadata).flatten(separator='/') + data = benedict(data).flatten(separator="/") + metadata = benedict(metadata).flatten(separator="/") if etcd_endpoint: try: - import_in_etcd(data=data, endpoint=etcd_endpoint, port=etcd_port, prefix=etcd_prefix) - spinner.text = click.style('Keys imported to etcd successfully.', fg=Colors.GREEN) + import_in_etcd( + data=data, endpoint=etcd_endpoint, port=etcd_port, prefix=etcd_prefix + ) + spinner.text = click.style( + "Keys imported to etcd successfully.", fg=Colors.GREEN + ) spinner.green.ok(Symbols.SUCCESS) return except Exception as e: - spinner.text = click.style(f'Error importing keys to etcd: {e}', fg=Colors.RED) + spinner.text = click.style( + f"Error importing keys to etcd: {e}", fg=Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e try: client = new_v2_client(with_project=(not with_org)) - with Revision(tree_name=tree_name, commit=commit, client=client, spinner=spinner, - with_org=with_org, milestone=milestone) as rev: + with Revision( + tree_name=tree_name, + commit=commit, + client=client, + spinner=spinner, + with_org=with_org, + milestone=milestone, + ) as rev: rev_id = rev.revision_id for key, value in data.items(): @@ -146,30 +214,32 @@ def import_keys( rev.store(key=key, value=str(value), perms=644, metadata=key_metadata) spinner.write( click.style( - '\t{} Key {} added.'.format(Symbols.SUCCESS, key), + "\t{} Key {} added.".format(Symbols.SUCCESS, key), fg=Colors.CYAN, ) ) if update_head: payload = { - 'kind': 'ConfigTree', - 'apiVersion': 'api.rapyuta.io/v2', - 'metadata': { - 'name': tree_name, + "kind": "ConfigTree", + "apiVersion": "api.rapyuta.io/v2", + "metadata": { + "name": tree_name, }, - 'head': { - 'metadata': { - 'guid': rev_id, + "head": { + "metadata": { + "guid": rev_id, } }, } client.set_revision_config_tree(tree_name, payload) - spinner.text = click.style('Config tree HEAD updated successfully.', fg=Colors.CYAN) + spinner.text = click.style( + "Config tree HEAD updated successfully.", fg=Colors.CYAN + ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style(f'Error importing keys: {e}', fg=Colors.RED) + spinner.text = click.style(f"Error importing keys: {e}", fg=Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -186,11 +256,15 @@ def split_metadata(data: Iterable) -> (Iterable, Iterable): content[key] = value continue - potential_content = value.get('value') - potential_meta = value.get('metadata') + potential_content = value.get("value") + potential_meta = value.get("metadata") - if (len(value) == 2 and potential_content is not None and - potential_meta is not None and isinstance(potential_meta, dict)): + if ( + len(value) == 2 + and potential_content is not None + and potential_meta is not None + and isinstance(potential_meta, dict) + ): content[key] = potential_content metadata[key] = Metadata(potential_meta) continue @@ -201,9 +275,9 @@ def split_metadata(data: Iterable) -> (Iterable, Iterable): def _process_files_with_overrides( - files: Iterable[str], - overrides: Iterable[str], - spinner: Yaspin, + files: Iterable[str], + overrides: Iterable[str], + spinner: Yaspin, ) -> (benedict, benedict): """Helper function to process the files and overrides. @@ -214,14 +288,14 @@ def _process_files_with_overrides( for f in files: file_prefix = Path(f).stem - file_format = 'yaml' - if f.endswith('json'): - file_format = 'json' + file_format = "yaml" + if f.endswith("json"): + file_format = "json" data[file_prefix] = benedict(f, format=file_format) spinner.write( click.style( - '{} File {} processed.'.format(Symbols.SUCCESS, f), + "{} File {} processed.".format(Symbols.SUCCESS, f), fg=Colors.CYAN, ) ) @@ -232,15 +306,15 @@ def _process_files_with_overrides( override = benedict({}) for f in overrides: - file_format = 'yaml' - if f.endswith('json'): - file_format = 'json' + file_format = "yaml" + if f.endswith("json"): + file_format = "json" - override.merge(benedict(f, format=file_format).unflatten(separator='/')) + override.merge(benedict(f, format=file_format).unflatten(separator="/")) spinner.write( click.style( - '{} Override file {} processed.'.format(Symbols.SUCCESS, f), + "{} Override file {} processed.".format(Symbols.SUCCESS, f), fg=Colors.CYAN, ) ) diff --git a/riocli/configtree/merge.py b/riocli/configtree/merge.py index aa0f5be6..0902e034 100644 --- a/riocli/configtree/merge.py +++ b/riocli/configtree/merge.py @@ -14,48 +14,85 @@ import os from tempfile import NamedTemporaryFile from typing import Optional -from benedict import benedict + import click +from benedict import benedict from click_help_colors import HelpColorsCommand from yaspin.core import Yaspin from riocli.config import get_config_from_context, new_v2_client from riocli.configtree.import_keys import split_metadata from riocli.configtree.revision import Revision -from riocli.configtree.util import Metadata, combine_metadata, fetch_last_milestone_keys, fetch_ref_keys, fetch_tree_keys, unflatten_keys +from riocli.configtree.util import ( + Metadata, + combine_metadata, + fetch_last_milestone_keys, + fetch_ref_keys, + fetch_tree_keys, + unflatten_keys, +) from riocli.constants.colors import Colors from riocli.constants.symbols import Symbols from riocli.utils.spinner import with_spinner @click.command( - 'merge', + "merge", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('base-tree-name', type=str) -@click.argument('ref', type=str) -@click.option('--silent', 'silent', is_flag=True, type=click.BOOL, default=False, - help='Skip interactively, if fast merge is not possible, then fail.') -@click.option('--ignore-conflict', 'ignore_conflict', is_flag=True, type=click.BOOL, - default=False, help='Skip the conflicting keys and only perform a partial fast merge.') -@click.option('--milestone', 'milestone', type=str, - help='Minestone name for the imported revision.') -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') +@click.argument("base-tree-name", type=str) +@click.argument("ref", type=str) +@click.option( + "--silent", + "silent", + is_flag=True, + type=click.BOOL, + default=False, + help="Skip interactively, if fast merge is not possible, then fail.", +) +@click.option( + "--ignore-conflict", + "ignore_conflict", + is_flag=True, + type=click.BOOL, + default=False, + help="Skip the conflicting keys and only perform a partial fast merge.", +) +@click.option( + "--milestone", + "milestone", + type=str, + help="Minestone name for the imported revision.", +) +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) @click.pass_context @with_spinner(text="Merging...") -def merge_revisions(ctx: click.Context, base_tree_name: str, ref: str, - silent: bool, with_org: bool, milestone: Optional[str], - ignore_conflict: bool, spinner: Yaspin): +def merge_revisions( + ctx: click.Context, + base_tree_name: str, + ref: str, + silent: bool, + with_org: bool, + milestone: Optional[str], + ignore_conflict: bool, + spinner: Yaspin, +): """ Merge the revision specified by the ref on the base-tree. The Base tree must be the name of the tree. Merge always works on the HEAD of the tree. The ref is a slash ('/') separated string. - * The first part can be 'org' or 'proj' defining the scope of the reference. + * The first part can be 'org' or 'proj' defining the scope of the reference. * The second part defines the name of the Tree. @@ -63,7 +100,7 @@ def merge_revisions(ctx: click.Context, base_tree_name: str, ref: str, Examples: - * org/tree-name + * org/tree-name * org/tree-name/rev-id @@ -79,11 +116,13 @@ def merge_revisions(ctx: click.Context, base_tree_name: str, ref: str, try: base_keys = fetch_tree_keys(is_org=with_org, tree_name=base_tree_name) source_keys = fetch_ref_keys(ref=ref) - old_base_keys = fetch_last_milestone_keys(is_org=with_org, tree_name=base_tree_name) + old_base_keys = fetch_last_milestone_keys( + is_org=with_org, tree_name=base_tree_name + ) fast_merge_possible = is_fast_merge_possible(base_keys, source_keys) if not fast_merge_possible and not ignore_conflict and silent: - raise Exception('Fast merge is not possible') + raise Exception("Fast merge is not possible") fast_merge(base_keys, source_keys) @@ -92,18 +131,26 @@ def merge_revisions(ctx: click.Context, base_tree_name: str, ref: str, merged = interactive_merge(ctx, base_keys, source_keys, old_base_keys) data, metadata = split_metadata(merged) else: - spinner.write(click.style('{} Fast-forwarding'.format(Symbols.INFO), fg=Colors.CYAN)) + spinner.write( + click.style("{} Fast-forwarding".format(Symbols.INFO), fg=Colors.CYAN) + ) # Combining and Splitting is required to remove the extra API fields # in the Value. base_keys = combine_metadata(base_keys) data, metadata = split_metadata(base_keys) - data = benedict(data).flatten(separator='/') - metadata = benedict(metadata).flatten(separator='/') + data = benedict(data).flatten(separator="/") + metadata = benedict(metadata).flatten(separator="/") client = new_v2_client(with_project=(not with_org)) - with Revision(tree_name=base_tree_name, client=client, force_new=True, - with_org=with_org, commit=True, milestone=milestone) as rev: + with Revision( + tree_name=base_tree_name, + client=client, + force_new=True, + with_org=with_org, + commit=True, + milestone=milestone, + ) as rev: rev_id = rev.revision_id for key, value in data.items(): @@ -114,20 +161,20 @@ def merge_revisions(ctx: click.Context, base_tree_name: str, ref: str, rev.store(key=key, value=str(value), perms=644, metadata=key_metadata) payload = { - 'kind': 'ConfigTree', - 'apiVersion': 'api.rapyuta.io/v2', - 'metadata': { - 'name': base_tree_name, + "kind": "ConfigTree", + "apiVersion": "api.rapyuta.io/v2", + "metadata": { + "name": base_tree_name, }, - 'head': { - 'metadata': { - 'guid': rev_id, + "head": { + "metadata": { + "guid": rev_id, } }, } client.set_revision_config_tree(base_tree_name, payload) - spinner.text = click.style('Config tree merged successfully.', fg=Colors.CYAN) + spinner.text = click.style("Config tree merged successfully.", fg=Colors.CYAN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: spinner.red.text = str(e) @@ -135,22 +182,25 @@ def merge_revisions(ctx: click.Context, base_tree_name: str, ref: str, raise SystemExit(1) from e -def interactive_merge(ctx: click.Context, keys_1: dict, keys_2: dict, keys_3: Optional[dict]) -> dict: +def interactive_merge( + ctx: click.Context, keys_1: dict, keys_2: dict, keys_3: Optional[dict] +) -> dict: cfg = get_config_from_context(ctx) keys_1 = unflatten_keys(keys_1) keys_2 = unflatten_keys(keys_2) keys_3 = unflatten_keys(keys_3) - with NamedTemporaryFile(mode='w+b', prefix='HEAD_') as file_1, \ - NamedTemporaryFile(mode='w+b', prefix='MERGE_HEAD_') as file_2, \ - NamedTemporaryFile(mode='w+b', prefix='PREV_BASE_') as file_3: - + with NamedTemporaryFile(mode="w+b", prefix="HEAD_") as file_1, NamedTemporaryFile( + mode="w+b", prefix="MERGE_HEAD_" + ) as file_2, NamedTemporaryFile(mode="w+b", prefix="PREV_BASE_") as file_3: keys_1.to_json(filepath=file_1.name, indent=4) keys_2.to_json(filepath=file_2.name, indent=4) keys_3.to_json(filepath=file_3.name, indent=4) - os.system('{} {} {} {}'.format(cfg.merge_tool, file_3.name, file_1.name, file_2.name)) + os.system( + "{} {} {} {}".format(cfg.merge_tool, file_3.name, file_1.name, file_2.name) + ) - return benedict(file_1.name, format='json') + return benedict(file_1.name, format="json") def is_fast_merge_possible(base: dict, source: dict) -> bool: @@ -159,11 +209,11 @@ def is_fast_merge_possible(base: dict, source: dict) -> bool: if not source_value: continue - source_data, base_data = source_value.get('data'), value.get('data') + source_data, base_data = source_value.get("data"), value.get("data") if source_data != base_data: return False - source_meta, base_meta = source_value.get('metadata'), value.get('metadata') + source_meta, base_meta = source_value.get("metadata"), value.get("metadata") if source_meta != base_meta: return False @@ -174,5 +224,3 @@ def fast_merge(base: dict, source: dict) -> None: for key, value in source.items(): if not base.get(key): base[key] = value - - diff --git a/riocli/configtree/revision.py b/riocli/configtree/revision.py index fcd3dccb..1cdca782 100644 --- a/riocli/configtree/revision.py +++ b/riocli/configtree/revision.py @@ -24,7 +24,12 @@ from riocli.config import get_config_from_context, new_v2_client from riocli.config.config import Configuration -from riocli.configtree.util import MILESTONE_LABEL_KEY, display_config_tree_keys, get_revision_from_state, save_revision +from riocli.configtree.util import ( + MILESTONE_LABEL_KEY, + display_config_tree_keys, + get_revision_from_state, + save_revision, +) from riocli.constants.colors import Colors from riocli.constants.symbols import Symbols from riocli.utils.spinner import with_spinner @@ -35,15 +40,17 @@ class Revision(object): _DEFAULT_COMMIT_MSG = "imported through rio-cli" - def __init__(self, tree_name: str, - client: Client, - rev_id: Optional[str] = None, - milestone: Optional[str] = None, - commit: bool = False, - force_new: bool = False, - spinner: Optional[Yaspin] = None, - with_org: bool = True): - + def __init__( + self, + tree_name: str, + client: Client, + rev_id: Optional[str] = None, + milestone: Optional[str] = None, + commit: bool = False, + force_new: bool = False, + spinner: Optional[Yaspin] = None, + with_org: bool = True, + ): self._tree_name = tree_name self._client = client self._commit = commit @@ -63,16 +70,24 @@ def __init__(self, tree_name: str, if rev_id is not None: self._rev_id = rev_id self._explicit = True - msg = '{} Using revision {}.'.format(Symbols.INFO, self._rev_id) + msg = "{} Using revision {}.".format(Symbols.INFO, self._rev_id) elif not force_new and rev and not rev.committed: self._rev_id = rev.rev_id - msg = '{} Re-using revision {}.'.format(Symbols.INFO, self._rev_id) + msg = "{} Re-using revision {}.".format(Symbols.INFO, self._rev_id) else: - self._rev = self._client.initialize_config_tree_revision(tree_name=self._tree_name) + self._rev = self._client.initialize_config_tree_revision( + tree_name=self._tree_name + ) self._rev_id = self._rev.metadata.guid - msg = '{} Revision {} created successfully.'.format(Symbols.SUCCESS, self._rev_id) - save_revision(org_guid=self._org_guid, project_guid=self._project_guid, tree_name=self._tree_name, - rev_id=self._rev_id) + msg = "{} Revision {} created successfully.".format( + Symbols.SUCCESS, self._rev_id + ) + save_revision( + org_guid=self._org_guid, + project_guid=self._project_guid, + tree_name=self._tree_name, + rev_id=self._rev_id, + ) if self._spinner: self._spinner.write(click.style(msg, fg=Colors.CYAN)) @@ -81,31 +96,42 @@ def __init__(self, tree_name: str, def revision_id(self: Revision) -> str: return self._rev_id - def store(self: Revision, key: str, value: str, perms: int = 644, metadata: Optional[dict] = None) -> None: + def store( + self: Revision, + key: str, + value: str, + perms: int = 644, + metadata: Optional[dict] = None, + ) -> None: str_val = str(value) - enc_val = str_val.encode('utf-8') + enc_val = str_val.encode("utf-8") data = { - 'permissions': str(perms), - 'checksum': md5(enc_val).hexdigest(), - 'contentType': 'kv', - 'contentLength': len(str_val), - 'data': b64encode(enc_val).decode(), + "permissions": str(perms), + "checksum": md5(enc_val).hexdigest(), + "contentType": "kv", + "contentLength": len(str_val), + "data": b64encode(enc_val).decode(), } if metadata is not None: - data['metadata'] = metadata + data["metadata"] = metadata self._data[key] = data def store_file(self: Revision, key: str, file_path: str) -> None: - self._client.store_file_in_revision(tree_name=self._tree_name, rev_id=self._rev_id, - key=key, file_path=file_path) + self._client.store_file_in_revision( + tree_name=self._tree_name, rev_id=self._rev_id, key=key, file_path=file_path + ) def delete(self: Revision, key: str) -> None: - self._client.delete_key_in_revision(tree_name=self._tree_name, rev_id=self._rev_id, key=key) + self._client.delete_key_in_revision( + tree_name=self._tree_name, rev_id=self._rev_id, key=key + ) - def commit(self: Revision, msg: Optional[str] = None, author: Optional[str] = None) -> None: + def commit( + self: Revision, msg: Optional[str] = None, author: Optional[str] = None + ) -> None: if msg is None: msg = self._DEFAULT_COMMIT_MSG @@ -113,29 +139,35 @@ def commit(self: Revision, msg: Optional[str] = None, author: Optional[str] = No author = self._get_author() payload: dict[str, Any] = { - 'kind': 'ConfigTreeRevision', - 'apiVersion': 'api.rapyuta.io/v2', - 'message': msg, - 'author': author, + "kind": "ConfigTreeRevision", + "apiVersion": "api.rapyuta.io/v2", + "message": msg, + "author": author, } if self._milestone is not None: - payload['metadata'] = { - 'labels': { + payload["metadata"] = { + "labels": { MILESTONE_LABEL_KEY: self._milestone, } } - self._client.commit_config_tree_revision(tree_name=self._tree_name, - rev_id=self._rev_id, payload=payload) + self._client.commit_config_tree_revision( + tree_name=self._tree_name, rev_id=self._rev_id, payload=payload + ) if not self._explicit: - save_revision(org_guid=self._org_guid, project_guid=self._project_guid, tree_name=self._tree_name, - rev_id=self._rev_id, committed=True) + save_revision( + org_guid=self._org_guid, + project_guid=self._project_guid, + tree_name=self._tree_name, + rev_id=self._rev_id, + committed=True, + ) if self._spinner: self._spinner.write( click.style( - '{} Revision {} committed.'.format(Symbols.SUCCESS, self._rev_id), + "{} Revision {} committed.".format(Symbols.SUCCESS, self._rev_id), fg=Colors.CYAN, ) ) @@ -148,14 +180,15 @@ def __exit__(self: Revision, typ: Type, val: Any, _: Any) -> None: raise val if self._data: - self._client.store_keys_in_revision(tree_name=self._tree_name, - rev_id=self._rev_id, payload=self._data) + self._client.store_keys_in_revision( + tree_name=self._tree_name, rev_id=self._rev_id, payload=self._data + ) if self._commit and self._rev_id: self.commit() def _get_author(self: Revision) -> str: - author = self._config.data.get('email_id', None) + author = self._config.data.get("email_id", None) if author is not None: return author @@ -163,7 +196,7 @@ def _get_author(self: Revision) -> str: @click.group( - name='revision', + name="revision", invoke_without_command=False, cls=HelpColorsGroup, help_headers_color=Colors.YELLOW, @@ -177,23 +210,29 @@ def revision() -> None: @click.command( - 'init', + "init", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('tree-name', type=str) -@click.option('--force', is_flag=True, type=bool) -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') +@click.argument("tree-name", type=str) +@click.option("--force", is_flag=True, type=bool) +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) @click.pass_context @with_spinner(text="Initializing Config tree revision...") def init_revision( - ctx: click.Context, - tree_name: str, - force: bool, - with_org: bool, - spinner: Yaspin, + ctx: click.Context, + tree_name: str, + force: bool, + with_org: bool, + spinner: Yaspin, ) -> None: """ Initialize a new revision for the Config tree @@ -203,14 +242,18 @@ def init_revision( if not with_org: project_guid = config.project_guid - rev = get_revision_from_state(org_guid=config.organization_guid, - project_guid=project_guid, - tree_name=tree_name) + rev = get_revision_from_state( + org_guid=config.organization_guid, + project_guid=project_guid, + tree_name=tree_name, + ) if not force and rev is not None and not rev.committed: spinner.text = click.style( - 'Revision {} is already present. Subsequent commands will re-use it. \n' - 'If you want to force create a new revision use the --force flag.'.format(rev.rev_id), + "Revision {} is already present. Subsequent commands will re-use it. \n" + "If you want to force create a new revision use the --force flag.".format( + rev.rev_id + ), fg=Colors.CYAN, ) spinner.green.ok(Symbols.INFO) @@ -218,37 +261,54 @@ def init_revision( try: client = new_v2_client(with_project=(not with_org)) - Revision(tree_name=tree_name, force_new=force, spinner=spinner, client=client, with_org=with_org) + Revision( + tree_name=tree_name, + force_new=force, + spinner=spinner, + client=client, + with_org=with_org, + ) except Exception as e: spinner.text = click.style( - 'Failed to initialize Config tree revision: {}'.format(e), Colors.RED) + "Failed to initialize Config tree revision: {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @click.command( - 'commit', + "commit", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('tree-name', type=str) -@click.argument('rev_id', type=str, required=False) -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') -@click.option('-m', '--message', 'message', type=str, help='Message for the Revision.') -@click.option('--milestone', 'milestone', type=str, - help='Minestone name for the imported revision.') +@click.argument("tree-name", type=str) +@click.argument("rev_id", type=str, required=False) +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) +@click.option("-m", "--message", "message", type=str, help="Message for the Revision.") +@click.option( + "--milestone", + "milestone", + type=str, + help="Minestone name for the imported revision.", +) @click.pass_context @with_spinner(text="Committing Config tree revision...") def commit_revision( - ctx: click.Context, - tree_name: str, - rev_id: str, - message: str, - milestone: Optional[str], - with_org: bool, - spinner: Yaspin, + ctx: click.Context, + tree_name: str, + rev_id: str, + message: str, + milestone: Optional[str], + with_org: bool, + spinner: Yaspin, ) -> None: """ Commit the existing Revision @@ -260,13 +320,15 @@ def commit_revision( project_guid = config.project_guid if not rev_id: - rev = get_revision_from_state(org_guid=config.organization_guid, - project_guid=project_guid, - tree_name=tree_name) + rev = get_revision_from_state( + org_guid=config.organization_guid, + project_guid=project_guid, + tree_name=tree_name, + ) if not rev or rev.committed: spinner.text = click.style( - 'RevisionID not provided as argument and not found in the State file.', + "RevisionID not provided as argument and not found in the State file.", fg=Colors.RED, ) spinner.red.fail(Symbols.ERROR) @@ -274,36 +336,49 @@ def commit_revision( try: client = new_v2_client(with_project=(not with_org)) - rev = Revision(tree_name=tree_name, rev_id=rev_id, spinner=spinner, - client=client, with_org=with_org, milestone=milestone) + rev = Revision( + tree_name=tree_name, + rev_id=rev_id, + spinner=spinner, + client=client, + with_org=with_org, + milestone=milestone, + ) rev.commit(msg=message) except Exception as e: spinner.text = click.style( - 'Failed to commit Config tree revision: {}'.format(e), Colors.RED) + "Failed to commit Config tree revision: {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @click.command( - 'put', + "put", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('tree-name', type=str) -@click.argument('key', type=str) -@click.argument('value', type=str) -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') +@click.argument("tree-name", type=str) +@click.argument("key", type=str) +@click.argument("value", type=str) +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) @click.pass_context @with_spinner(text="Adding key to Config tree revision...") def put_key_in_revision( - ctx: click.Context, - tree_name: str, - key: str, - value: str, - with_org: bool, - spinner: Yaspin, + ctx: click.Context, + tree_name: str, + key: str, + value: str, + with_org: bool, + spinner: Yaspin, ) -> None: """ Put a key in the uncommitted revision. @@ -314,14 +389,16 @@ def put_key_in_revision( if not with_org: project_guid = config.project_guid - rev = get_revision_from_state(org_guid=config.organization_guid, - project_guid=project_guid, - tree_name=tree_name) + rev = get_revision_from_state( + org_guid=config.organization_guid, + project_guid=project_guid, + tree_name=tree_name, + ) if not rev or rev.committed: spinner.text = click.style( - 'RevisionID not provided as argument and not found in the State file. \n' - 'Start a new commit using the `init` command.', + "RevisionID not provided as argument and not found in the State file. \n" + "Start a new commit using the `init` command.", fg=Colors.RED, ) spinner.red.fail(Symbols.ERROR) @@ -329,39 +406,45 @@ def put_key_in_revision( try: client = new_v2_client(with_project=(not with_org)) - with Revision(tree_name=tree_name, spinner=spinner, client=client, - with_org=with_org) as rev: + with Revision( + tree_name=tree_name, spinner=spinner, client=client, with_org=with_org + ) as rev: rev.store(key=key, value=value) - spinner.write(click.style( - '\t{} Key {} added.'.format(Symbols.SUCCESS, key) - )) + spinner.write(click.style("\t{} Key {} added.".format(Symbols.SUCCESS, key))) except Exception as e: spinner.text = click.style( - 'Failed to put key in Config tree revision: {}'.format(e), Colors.RED) + "Failed to put key in Config tree revision: {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @click.command( - 'put-file', + "put-file", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('tree-name', type=str) -@click.argument('key', type=str) -@click.argument('file-path', type=str) -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') +@click.argument("tree-name", type=str) +@click.argument("key", type=str) +@click.argument("file-path", type=str) +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) @click.pass_context @with_spinner(text="Adding key to Config tree revision...") def put_file_in_revision( - ctx: click.Context, - tree_name: str, - key: str, - file_path: str, - with_org: bool, - spinner: Yaspin, + ctx: click.Context, + tree_name: str, + key: str, + file_path: str, + with_org: bool, + spinner: Yaspin, ) -> None: """ Upload a file in the uncommitted revision. @@ -372,14 +455,16 @@ def put_file_in_revision( if not with_org: project_guid = config.project_guid - rev = get_revision_from_state(org_guid=config.organization_guid, - project_guid=project_guid, - tree_name=tree_name) + rev = get_revision_from_state( + org_guid=config.organization_guid, + project_guid=project_guid, + tree_name=tree_name, + ) if not rev or rev.committed: spinner.text = click.style( - 'RevisionID not provided as argument and not found in the State file. \n' - 'Start a new commit using the `init` command.', + "RevisionID not provided as argument and not found in the State file. \n" + "Start a new commit using the `init` command.", fg=Colors.RED, ) spinner.red.fail(Symbols.ERROR) @@ -387,37 +472,43 @@ def put_file_in_revision( try: client = new_v2_client(with_project=(not with_org)) - with Revision(tree_name=tree_name, spinner=spinner, client=client, - with_org=with_org) as rev: + with Revision( + tree_name=tree_name, spinner=spinner, client=client, with_org=with_org + ) as rev: rev.store_file(key=key, file_path=file_path) - spinner.write(click.style( - '\t{} File {} added.'.format(Symbols.SUCCESS, key) - )) + spinner.write(click.style("\t{} File {} added.".format(Symbols.SUCCESS, key))) except Exception as e: spinner.text = click.style( - 'Failed to put-file in Config tree revision: {}'.format(e), Colors.RED) + "Failed to put-file in Config tree revision: {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @click.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('tree-name', type=str) -@click.argument('key', type=str) -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') +@click.argument("tree-name", type=str) +@click.argument("key", type=str) +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) @click.pass_context @with_spinner(text="Deleting key to Config tree revision...") def delete_key_in_revision( - ctx: click.Context, - tree_name: str, - key: str, - with_org: bool, - spinner: Yaspin, + ctx: click.Context, + tree_name: str, + key: str, + with_org: bool, + spinner: Yaspin, ) -> None: """ Delete the key in the uncommitted revision @@ -427,14 +518,16 @@ def delete_key_in_revision( if not with_org: project_guid = config.project_guid - rev = get_revision_from_state(org_guid=config.organization_guid, - project_guid=project_guid, - tree_name=tree_name) + rev = get_revision_from_state( + org_guid=config.organization_guid, + project_guid=project_guid, + tree_name=tree_name, + ) if not rev or rev.committed: spinner.text = click.style( - 'RevisionID not provided as argument and not found in the State file. \n' - 'Start a new commit using the `init` command.', + "RevisionID not provided as argument and not found in the State file. \n" + "Start a new commit using the `init` command.", fg=Colors.RED, ) spinner.red.fail(Symbols.ERROR) @@ -442,35 +535,43 @@ def delete_key_in_revision( try: client = new_v2_client(with_project=(not with_org)) - with Revision(tree_name=tree_name, spinner=spinner, client=client, - with_org=with_org) as rev: + with Revision( + tree_name=tree_name, spinner=spinner, client=client, with_org=with_org + ) as rev: rev.delete(key=key) - spinner.write(click.style( - '\t{} Key {} removed.'.format(Symbols.SUCCESS, key) - )) + spinner.write( + click.style("\t{} Key {} removed.".format(Symbols.SUCCESS, key)) + ) except Exception as e: spinner.text = click.style( - 'Failed to delete key in Config tree revision: {}'.format(e), Colors.RED) + "Failed to delete key in Config tree revision: {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @click.command( - 'keys', + "keys", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('tree-name', type=str) -@click.argument('rev-id', type=str, required=False) -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') +@click.argument("tree-name", type=str) +@click.argument("rev-id", type=str, required=False) +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) @click.pass_context def list_revision_keys( - ctx: click.Context, - tree_name: str, - rev_id: Optional[str], - with_org: bool, + ctx: click.Context, + tree_name: str, + rev_id: Optional[str], + with_org: bool, ) -> None: """ Lists all the keys in the revision @@ -481,14 +582,16 @@ def list_revision_keys( if not with_org: project_guid = config.project_guid - rev = get_revision_from_state(org_guid=config.organization_guid, - project_guid=project_guid, - tree_name=tree_name) + rev = get_revision_from_state( + org_guid=config.organization_guid, + project_guid=project_guid, + tree_name=tree_name, + ) if not rev or rev.committed: click.echo( click.style( - 'RevisionID not provided as argument and not found in the State file.', + "RevisionID not provided as argument and not found in the State file.", fg=Colors.RED, ) ) @@ -500,9 +603,9 @@ def list_revision_keys( client = new_v2_client(with_project=(not with_org)) tree = client.get_config_tree(tree_name=tree_name, rev_id=rev_id) - keys = tree.get('keys') + keys = tree.get("keys") if not isinstance(keys, dict): - raise Exception('Keys are not dictionary') + raise Exception("Keys are not dictionary") display_config_tree_keys(keys=keys) except Exception as e: diff --git a/riocli/configtree/tree.py b/riocli/configtree/tree.py index 70def5b3..96d8514c 100644 --- a/riocli/configtree/tree.py +++ b/riocli/configtree/tree.py @@ -19,28 +19,40 @@ from yaspin.core import Yaspin from riocli.config import get_config_from_context, new_v2_client -from riocli.configtree.util import display_config_tree_keys, display_config_tree_revision_graph, \ - display_config_tree_revisions, display_config_trees, fetch_tree_keys, get_revision_from_state +from riocli.configtree.util import ( + display_config_tree_keys, + display_config_tree_revision_graph, + display_config_tree_revisions, + display_config_trees, + fetch_tree_keys, + get_revision_from_state, +) from riocli.constants import Symbols, Colors from riocli.utils.spinner import with_spinner @click.command( - 'create', + "create", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('tree-name', type=str) -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') +@click.argument("tree-name", type=str) +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) @click.pass_context @with_spinner(text="Creating Config tree...") def create_config_tree( - ctx: click.Context, - tree_name: str, - with_org: bool, - spinner: Yaspin, + ctx: click.Context, + tree_name: str, + with_org: bool, + spinner: Yaspin, ) -> None: """ Creates a new Config tree. @@ -55,37 +67,44 @@ def create_config_tree( "apiVersion": "api.rapyuta.io/v2", "metadata": { "name": tree_name, - } + }, } config_tree = client.create_config_tree(payload) spinner.text = click.style( - 'Config tree {} created successfully.'.format(config_tree.metadata.name), - fg=Colors.GREEN + "Config tree {} created successfully.".format(config_tree.metadata.name), + fg=Colors.GREEN, ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: spinner.text = click.style( - 'Failed to create Config tree: {}'.format(e), Colors.RED) + "Failed to create Config tree: {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @click.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('tree-name', type=str) -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') +@click.argument("tree-name", type=str) +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) @click.pass_context @with_spinner(text="Deleting Config tree...") def delete_config_tree( - _: click.Context, - tree_name: str, - with_org: bool, - spinner: Yaspin, + _: click.Context, + tree_name: str, + with_org: bool, + spinner: Yaspin, ) -> None: """ Deletes the Config tree. @@ -94,35 +113,40 @@ def delete_config_tree( try: client = new_v2_client(with_project=(not with_org)) client.delete_config_tree(tree_name) - spinner.text = click.style( - 'Config tree deleted successfully.', fg=Colors.GREEN - ) + spinner.text = click.style("Config tree deleted successfully.", fg=Colors.GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: spinner.text = click.style( - 'Failed to delete Config tree: {}'.format(e), Colors.RED) + "Failed to delete Config tree: {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @click.command( - 'clone', + "clone", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('tree-name', type=str) -@click.argument('new-tree', type=str) -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') +@click.argument("tree-name", type=str) +@click.argument("new-tree", type=str) +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) @click.pass_context @with_spinner(text="Cloning Config tree...") def clone_tree( - _: click.Context, - tree_name: str, - new_tree: str, - with_org: bool, - spinner: Yaspin, + _: click.Context, + tree_name: str, + new_tree: str, + with_org: bool, + spinner: Yaspin, ) -> None: """ Clones an existing Config tree. @@ -133,20 +157,20 @@ def clone_tree( "apiVersion": "api.rapyuta.io/v2", "metadata": { "name": new_tree, - } + }, } try: client = new_v2_client(with_project=(not with_org)) tree = client.get_config_tree(tree_name=tree_name) - head = tree.get('head') + head = tree.get("head") if head is not None: - payload['head'] = head + payload["head"] = head else: spinner.write( click.style( - 'The head of the given Config tree is empty. Creating a new Config tree instead', + "The head of the given Config tree is empty. Creating a new Config tree instead", fg=Colors.YELLOW, ) ) @@ -156,35 +180,42 @@ def clone_tree( client = new_v2_client(with_project=True) config_tree = client.create_config_tree(payload) spinner.text = click.style( - 'Config tree {} cloned successfully.'.format(config_tree.metadata.name), - fg=Colors.GREEN + "Config tree {} cloned successfully.".format(config_tree.metadata.name), + fg=Colors.GREEN, ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: spinner.text = click.style( - 'Failed to clone Config tree: {}'.format(e), Colors.RED) + "Failed to clone Config tree: {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @click.command( - 'set-revision', + "set-revision", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('tree-name', type=str) -@click.argument('rev-id', type=str, required=False) -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') +@click.argument("tree-name", type=str) +@click.argument("rev-id", type=str, required=False) +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) @click.pass_context @with_spinner(text="Setting Config tree head...") def set_tree_revision( - ctx: click.Context, - tree_name: str, - rev_id: Optional[str], - with_org: bool, - spinner: Yaspin, + ctx: click.Context, + tree_name: str, + rev_id: Optional[str], + with_org: bool, + spinner: Yaspin, ) -> None: """ Sets the revision as head of the Config tree. @@ -196,13 +227,15 @@ def set_tree_revision( project_guid = config.project_guid if not rev_id: - rev = get_revision_from_state(org_guid=config.organization_guid, - project_guid=project_guid, - tree_name=tree_name) + rev = get_revision_from_state( + org_guid=config.organization_guid, + project_guid=project_guid, + tree_name=tree_name, + ) if not rev or not rev.committed: spinner.text = click.style( - 'RevisionID not provided as argument and not found in the State file.', + "RevisionID not provided as argument and not found in the State file.", fg=Colors.RED, ) spinner.red.fail(Symbols.ERROR) @@ -227,28 +260,35 @@ def set_tree_revision( client = new_v2_client(with_project=(not with_org)) client.set_revision_config_tree(tree_name, payload) spinner.text = click.style( - 'Config tree head updated successfully.', fg=Colors.GREEN + "Config tree head updated successfully.", fg=Colors.GREEN ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: spinner.text = click.style( - 'Failed to update config-tree Head: {}'.format(e), Colors.RED) + "Failed to update config-tree Head: {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @click.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) @click.pass_context def list_config_trees( - _: click.Context, - with_org: bool, + _: click.Context, + with_org: bool, ) -> None: """ Lists the Config trees. @@ -258,7 +298,7 @@ def list_config_trees( client = new_v2_client(with_project=(not with_org)) trees = client.list_config_trees() if not isinstance(trees, Iterable): - raise Exception('List items are not iterable') + raise Exception("List items are not iterable") display_config_trees(trees=trees) except Exception as e: @@ -267,21 +307,27 @@ def list_config_trees( @click.command( - 'keys', + "keys", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('tree-name', type=str) -@click.argument('rev-id', type=str, required=False) -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') +@click.argument("tree-name", type=str) +@click.argument("rev-id", type=str, required=False) +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) @click.pass_context def list_config_tree_keys( - _: click.Context, - tree_name: str, - rev_id: Optional[str], - with_org: bool, + _: click.Context, + tree_name: str, + rev_id: Optional[str], + with_org: bool, ) -> None: """ Lists all the keys in the Config tree. @@ -295,21 +341,27 @@ def list_config_tree_keys( @click.command( - 'history', + "history", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('tree-name', type=str) -@click.option('--graph', is_flag=True, type=bool, help='Display in the graph format.') -@click.option('--organization', 'with_org', is_flag=True, type=bool, - default=False, help='Operate on organization-scoped Config Trees only.') +@click.argument("tree-name", type=str) +@click.option("--graph", is_flag=True, type=bool, help="Display in the graph format.") +@click.option( + "--organization", + "with_org", + is_flag=True, + type=bool, + default=False, + help="Operate on organization-scoped Config Trees only.", +) @click.pass_context def list_tree_revisions( - _: click.Context, - graph: bool, - tree_name: str, - with_org: bool, + _: click.Context, + graph: bool, + tree_name: str, + with_org: bool, ) -> None: """ Shows the revisions of the Config Tree. @@ -318,7 +370,7 @@ def list_tree_revisions( client = new_v2_client(with_project=(not with_org)) revisions = client.list_config_tree_revisions(tree_name=tree_name) if not isinstance(revisions, Iterable): - raise Exception('List items are not iterable') + raise Exception("List items are not iterable") if graph: display_config_tree_revision_graph(tree_name=tree_name, revisions=revisions) diff --git a/riocli/configtree/util.py b/riocli/configtree/util.py index c10e70dc..74a8b210 100644 --- a/riocli/configtree/util.py +++ b/riocli/configtree/util.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations -from base64 import b64decode + import os +from base64 import b64decode from typing import Optional, Iterable -import yaml +import yaml from benedict import benedict from munch import Munch, munchify, unmunchify @@ -46,10 +47,13 @@ # } # } -def get_revision_from_state(org_guid: str, project_guid: Optional[str], tree_name: str) -> Optional[Munch]: + +def get_revision_from_state( + org_guid: str, project_guid: Optional[str], tree_name: str +) -> Optional[Munch]: s = StateFile() - if not s.state.get('configtrees'): + if not s.state.get("configtrees"): return # Either OrgGUID or ProjectGUID will be the Top-Level Key. @@ -68,7 +72,13 @@ def get_revision_from_state(org_guid: str, project_guid: Optional[str], tree_nam return tree -def save_revision(org_guid: str, project_guid: Optional[str], tree_name: str, rev_id: str, committed: bool = False) -> None: +def save_revision( + org_guid: str, + project_guid: Optional[str], + tree_name: str, + rev_id: str, + committed: bool = False, +) -> None: s = StateFile() # Either OrgGUID or ProjectGUID will be the Top-Level Key. @@ -76,7 +86,7 @@ def save_revision(org_guid: str, project_guid: Optional[str], tree_name: str, re if project_guid is not None: top_level = project_guid - configtrees = s.state.get('configtrees') + configtrees = s.state.get("configtrees") if configtrees is None: configtrees = dict() @@ -84,7 +94,7 @@ def save_revision(org_guid: str, project_guid: Optional[str], tree_name: str, re if top_level_trees is None: top_level_trees = dict() - tree = {'rev_id': rev_id, 'committed': committed} + tree = {"rev_id": rev_id, "committed": committed} top_level_trees[tree_name] = tree configtrees[top_level] = top_level_trees @@ -95,12 +105,12 @@ def save_revision(org_guid: str, project_guid: Optional[str], tree_name: str, re def display_config_trees(trees: Iterable, show_header: bool = True) -> None: headers = [] if show_header: - headers = ['Tree Name', 'Head'] + headers = ["Tree Name", "Head"] data = [] for tree in trees: head = None - if tree.get('head'): + if tree.get("head"): head = tree.head.metadata.guid data.append([tree.metadata.name, head]) @@ -111,15 +121,15 @@ def display_config_trees(trees: Iterable, show_header: bool = True) -> None: def display_config_tree_keys(keys: dict, show_header: bool = True) -> None: headers = [] if show_header: - headers = ['Key', 'Metadata', 'Checksum'] + headers = ["Key", "Metadata", "Checksum"] data = [] for key, val in keys.items(): - metadata = val.get('metadata', None) + metadata = val.get("metadata", None) if isinstance(metadata, Munch): metadata = unmunchify(metadata) - data.append([key, metadata, val.get('checksum')]) + data.append([key, metadata, val.get("checksum")]) tabulate_data(data, headers=headers) @@ -127,16 +137,32 @@ def display_config_tree_keys(keys: dict, show_header: bool = True) -> None: def display_config_tree_revisions(revisions: Iterable, show_header: bool = True) -> None: headers = [] if show_header: - headers = ['Updated At', 'RevisionID', 'Message', 'Milestone', 'Parent', 'Committed'] + headers = [ + "Updated At", + "RevisionID", + "Message", + "Milestone", + "Parent", + "Committed", + ] data = [] for rev in revisions: - message = rev.get('message') - parent = rev.get('parent') - committed = rev.get('committed', False) + message = rev.get("message") + parent = rev.get("parent") + committed = rev.get("committed", False) milestone = get_revision_milestone(rev) - data.append([rev.metadata.updatedAt, rev.metadata.guid, message, milestone, parent, committed]) + data.append( + [ + rev.metadata.updatedAt, + rev.metadata.guid, + message, + milestone, + parent, + committed, + ] + ) tabulate_data(data, headers=headers) @@ -146,12 +172,12 @@ def display_config_tree_revision_graph(tree_name: str, revisions: Iterable) -> N for rev in revisions: rev_id = rev.metadata.guid - parent = rev.get('parent') - message = rev.get('message') - if not rev.get('committed', False): - message = 'Uncommitted' + parent = rev.get("parent") + message = rev.get("message") + if not rev.get("committed", False): + message = "Uncommitted" - g.node(key=rev_id, label='{} {}'.format(rev_id, message)) + g.node(key=rev_id, label="{} {}".format(rev_id, message)) if parent is not None: g.edge(from_node=parent, to_node=rev_id) @@ -167,18 +193,18 @@ def get_dict(self: Metadata) -> dict: return self.data -def export_to_files(base_dir: str, data: dict, file_format: str = 'yaml') -> None: +def export_to_files(base_dir: str, data: dict, file_format: str = "yaml") -> None: base_dir = os.path.abspath(base_dir) for file_name, file_data in data.items(): - file_path = os.path.join(base_dir, '{}.{}'.format(file_name, file_format)) + file_path = os.path.join(base_dir, "{}.{}".format(file_name, file_format)) final_data = benedict(file_data) - if file_format == 'yaml': + if file_format == "yaml": final_data.to_yaml(filepath=file_path) - elif file_format == 'json': + elif file_format == "json": final_data.to_json(filepath=file_path, indent=4) else: - raise Exception('file_format is not supported') + raise Exception("file_format is not supported") def parse_ref(input: str) -> (bool, str, Optional[str]): @@ -205,22 +231,22 @@ def parse_ref(input: str) -> (bool, str, Optional[str]): * proj/tree-name/milestone """ - splits = input.split('/', maxsplit=2) + splits = input.split("/", maxsplit=2) if len(splits) < 2: - raise Exception('ref {} is invalid'.format(input)) + raise Exception("ref {} is invalid".format(input)) - if splits[0] not in ('org', 'proj'): - raise Exception('ref scope {} is invalid'.format(splits[0])) + if splits[0] not in ("org", "proj"): + raise Exception("ref scope {} is invalid".format(splits[0])) - is_org = splits[0] == 'org' + is_org = splits[0] == "org" revision = None if len(splits) == 3: revision = splits[2] milestone = None - if revision is not None and not revision.startswith('rev-'): + if revision is not None and not revision.startswith("rev-"): milestone = revision revision = None @@ -232,24 +258,27 @@ def unflatten_keys(keys: Optional[dict]) -> benedict: return benedict() data = combine_metadata(keys) - return benedict(data).unflatten(separator='/') + return benedict(data).unflatten(separator="/") def combine_metadata(keys: dict) -> dict: result = {} for key, val in keys.items(): - data = val.get('data', None) + data = val.get("data", None) if data is not None: - data = b64decode(data).decode('utf-8') + data = b64decode(data).decode("utf-8") # The data received from the API is always in string format. To use # appropriate data-type in Python (as well in exports), we are # passing it through YAML parser. data = yaml.safe_load(data) - metadata = val.get('metadata', None) + metadata = val.get("metadata", None) if metadata: - result[key] = {'value': data, 'metadata': metadata,} + result[key] = { + "value": data, + "metadata": metadata, + } else: result[key] = data @@ -265,57 +294,69 @@ def fetch_last_milestone_keys(is_org: bool, tree_name: str) -> Optional[dict]: for rev in revisions: milestone = get_revision_milestone(rev) if milestone is not None: - return fetch_tree_keys(is_org=is_org, tree_name=tree_name, - rev_id=rev.metadata.guid) + return fetch_tree_keys( + is_org=is_org, tree_name=tree_name, rev_id=rev.metadata.guid + ) def fetch_ref_keys(ref: str) -> dict: is_org, tree, rev_id, milestone = parse_ref(ref) - return fetch_tree_keys(is_org=is_org, tree_name=tree, rev_id=rev_id, - milestone=milestone) + return fetch_tree_keys( + is_org=is_org, tree_name=tree, rev_id=rev_id, milestone=milestone + ) -def fetch_tree_keys(is_org: bool, tree_name: str, - rev_id: Optional[str] = None, - milestone: Optional[str] = None) -> dict: +def fetch_tree_keys( + is_org: bool, + tree_name: str, + rev_id: Optional[str] = None, + milestone: Optional[str] = None, +) -> dict: if milestone: - rev_id = fetch_milestone_revision_id(is_org=is_org, tree_name=tree_name, - milestone=milestone) + rev_id = fetch_milestone_revision_id( + is_org=is_org, tree_name=tree_name, milestone=milestone + ) client = new_v2_client(with_project=(not is_org)) - tree = client.get_config_tree(tree_name=tree_name, rev_id=rev_id, include_data=True, - filter_content_types=['kv']) + tree = client.get_config_tree( + tree_name=tree_name, + rev_id=rev_id, + include_data=True, + filter_content_types=["kv"], + ) - if not tree.get('head'): - raise Exception('Config tree {} does not have keys in the revision'.format(tree)) + if not tree.get("head"): + raise Exception("Config tree {} does not have keys in the revision".format(tree)) - keys = tree.get('keys') + keys = tree.get("keys") if not isinstance(keys, dict): - raise Exception('Keys are not dictionary') + raise Exception("Keys are not dictionary") return keys def fetch_milestone_revision_id(is_org: bool, tree_name: str, milestone: str) -> str: client = new_v2_client(with_project=(not is_org)) - labels = '{}={}'.format(MILESTONE_LABEL_KEY, milestone) + labels = "{}={}".format(MILESTONE_LABEL_KEY, milestone) revisions = client.list_config_tree_revisions(tree_name=tree_name, labels=labels) if len(revisions) == 0: - raise Exception('Revision with milestone {} not found'.format(milestone)) + raise Exception("Revision with milestone {} not found".format(milestone)) if len(revisions) > 1: - raise Exception('More than one revision with milestone {} exists'.format(milestone)) + raise Exception( + "More than one revision with milestone {} exists".format(milestone) + ) return revisions[0].metadata.guid def get_revision_milestone(rev: dict) -> Optional[str]: - metadata = rev.get('metadata', None) + metadata = rev.get("metadata", None) if metadata is None: return - labels = metadata.get('labels', None) + labels = metadata.get("labels", None) if labels is None: return diff --git a/riocli/constants/colors.py b/riocli/constants/colors.py index 1b35d4ba..371fdd52 100644 --- a/riocli/constants/colors.py +++ b/riocli/constants/colors.py @@ -20,22 +20,23 @@ class Colors(str, Enum): Colors is a str enum based on the colors supported by click. https://github.com/pallets/click/blob/main/examples/colors/colors.py """ + def __str__(self): return str(self.value).lower() - BLACK = 'black' - RED = 'red' - GREEN = 'green' - YELLOW = 'yellow' - BLUE = 'blue' - MAGENTA = 'magenta' - CYAN = 'cyan' - WHITE = 'white' - BRIGHT_BLACK = 'bright_black' - BRIGHT_RED = 'bright_red' - BRIGHT_GREEN = 'bright_green' - BRIGHT_YELLOW = 'bright_yellow' - BRIGHT_BLUE = 'bright_blue' - BRIGHT_MAGENTA = 'bright_magenta' - BRIGHT_CYAN = 'bright_cyan' - BRIGHT_WHITE = 'bright_white' + BLACK = "black" + RED = "red" + GREEN = "green" + YELLOW = "yellow" + BLUE = "blue" + MAGENTA = "magenta" + CYAN = "cyan" + WHITE = "white" + BRIGHT_BLACK = "bright_black" + BRIGHT_RED = "bright_red" + BRIGHT_GREEN = "bright_green" + BRIGHT_YELLOW = "bright_yellow" + BRIGHT_BLUE = "bright_blue" + BRIGHT_MAGENTA = "bright_magenta" + BRIGHT_CYAN = "bright_cyan" + BRIGHT_WHITE = "bright_white" diff --git a/riocli/constants/regions.py b/riocli/constants/regions.py index a416414e..b622572a 100644 --- a/riocli/constants/regions.py +++ b/riocli/constants/regions.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. + class Regions: - JP = 'jp' - US = 'us' \ No newline at end of file + JP = "jp" + US = "us" diff --git a/riocli/constants/status.py b/riocli/constants/status.py index 8c66a560..e401ec6e 100644 --- a/riocli/constants/status.py +++ b/riocli/constants/status.py @@ -16,19 +16,17 @@ class Status(str, Enum): - def __str__(self): return str(self.value).lower() - RUNNING = 'Running' - AVAILABLE = 'Available' + RUNNING = "Running" + AVAILABLE = "Available" class ApplyResult(str, Enum): - def __str__(self): return str(self.value) - CREATED = 'Created' - UPDATED = 'Updated' - EXISTS = 'Exists' + CREATED = "Created" + UPDATED = "Updated" + EXISTS = "Exists" diff --git a/riocli/constants/symbols.py b/riocli/constants/symbols.py index cda687e0..5b74d7c9 100644 --- a/riocli/constants/symbols.py +++ b/riocli/constants/symbols.py @@ -12,9 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. + class Symbols: - INFO = 'ℹ️' - ERROR = '❌' - SUCCESS = '✅' - WARNING = '⚠️' - WAITING = '⏳' + INFO = "ℹ️" + ERROR = "❌" + SUCCESS = "✅" + WARNING = "⚠️" + WAITING = "⏳" diff --git a/riocli/deployment/delete.py b/riocli/deployment/delete.py index 224989bc..493b1bb6 100644 --- a/riocli/deployment/delete.py +++ b/riocli/deployment/delete.py @@ -28,26 +28,38 @@ @click.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--force', '-f', '--silent', is_flag=True, default=False, - help='Skip confirmation') -@click.option('-a', '--all', 'delete_all', is_flag=True, default=False, - help='Deletes all deployments in the project') -@click.option('--workers', '-w', - help="Number of parallel workers while running delete deployment " - "command. Defaults to 10.", type=int, default=10) -@click.argument('deployment-name-or-regex', type=str, default="") +@click.option( + "--force", "-f", "--silent", is_flag=True, default=False, help="Skip confirmation" +) +@click.option( + "-a", + "--all", + "delete_all", + is_flag=True, + default=False, + help="Deletes all deployments in the project", +) +@click.option( + "--workers", + "-w", + help="Number of parallel workers while running delete deployment " + "command. Defaults to 10.", + type=int, + default=10, +) +@click.argument("deployment-name-or-regex", type=str, default="") @with_spinner(text="Deleting deployment...") def delete_deployment( - force: bool, - deployment_name_or_regex: str, - delete_all: bool = False, - workers: int = 10, - spinner=None, + force: bool, + deployment_name_or_regex: str, + delete_all: bool = False, + workers: int = 10, + spinner=None, ) -> None: """Delete one or more deployments with a name or a regex pattern. @@ -85,11 +97,11 @@ def delete_deployment( return try: - deployments = fetch_deployments( - client, deployment_name_or_regex, delete_all) + deployments = fetch_deployments(client, deployment_name_or_regex, delete_all) except Exception as e: spinner.text = click.style( - 'Failed to delete deployment(s): {}'.format(e), Colors.RED) + "Failed to delete deployment(s): {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -101,18 +113,17 @@ def delete_deployment( with spinner.hidden(): print_deployments_for_confirmation(deployments) - spinner.write('') + spinner.write("") if not force: with spinner.hidden(): - click.confirm('Do you want to delete the above deployment(s)?', abort=True) - spinner.write('') + click.confirm("Do you want to delete the above deployment(s)?", abort=True) + spinner.write("") try: f = functools.partial(_apply_delete, client) result = apply_func_with_result( - f=f, items=deployments, - workers=workers, key=lambda x: x[0] + f=f, items=deployments, workers=workers, key=lambda x: x[0] ) data, statuses = [], [] @@ -120,18 +131,17 @@ def delete_deployment( fg = Colors.GREEN if status else Colors.RED icon = Symbols.SUCCESS if status else Symbols.ERROR statuses.append(status) - data.append([ - click.style(name, fg), - click.style('{} {}'.format(icon, msg), fg) - ]) + data.append( + [click.style(name, fg), click.style("{} {}".format(icon, msg), fg)] + ) with spinner.hidden(): - tabulate_data(data, headers=['Name', 'Status']) + tabulate_data(data, headers=["Name", "Status"]) # When no deployment is deleted, raise an exception. if not any(statuses): - spinner.write('') - spinner.text = click.style('Failed to delete deployment(s).', Colors.RED) + spinner.write("") + spinner.text = click.style("Failed to delete deployment(s).", Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) @@ -139,13 +149,13 @@ def delete_deployment( fg = Colors.GREEN if all(statuses) else Colors.YELLOW text = "successfully" if all(statuses) else "partially" - spinner.write('') - spinner.text = click.style( - 'Deployment(s) deleted {}.'.format(text), fg) + spinner.write("") + spinner.text = click.style("Deployment(s) deleted {}.".format(text), fg) spinner.ok(click.style(icon, fg)) except Exception as e: spinner.text = click.style( - 'Failed to delete deployment(s): {}'.format(e), Colors.RED) + "Failed to delete deployment(s): {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -153,6 +163,6 @@ def delete_deployment( def _apply_delete(client: Client, result: Queue, deployment: Deployment) -> None: try: client.delete_deployment(name=deployment.metadata.name) - result.put((deployment.metadata.name, True, 'Deployment Deleted Successfully')) + result.put((deployment.metadata.name, True, "Deployment Deleted Successfully")) except Exception as e: result.put((deployment.metadata.name, False, str(e))) diff --git a/riocli/deployment/execute.py b/riocli/deployment/execute.py index d1556720..b149a325 100644 --- a/riocli/deployment/execute.py +++ b/riocli/deployment/execute.py @@ -29,23 +29,24 @@ @click.command( - 'execute', + "execute", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--user', default='root') -@click.option('--shell', default='/bin/bash') -@click.option('--exec', 'exec_name', default=None, - help='Name of a executable in the component') -@click.argument('deployment-name', type=str) -@click.argument('command', nargs=-1) +@click.option("--user", default="root") +@click.option("--shell", default="/bin/bash") +@click.option( + "--exec", "exec_name", default=None, help="Name of a executable in the component" +) +@click.argument("deployment-name", type=str) +@click.argument("command", nargs=-1) def execute_command( - user: str, - shell: str, - exec_name: str, - deployment_name: str, - command: typing.List[str] + user: str, + shell: str, + exec_name: str, + deployment_name: str, + command: typing.List[str], ) -> None: """Execute a command on a device deployment @@ -78,27 +79,35 @@ def execute_command( deployment = client.get_deployment(deployment_name) if not deployment: - click.secho(f'{Symbols.ERROR} Deployment `{deployment_name}` not found', fg=Colors.RED) + click.secho( + f"{Symbols.ERROR} Deployment `{deployment_name}` not found", + fg=Colors.RED, + ) raise SystemExit(1) if deployment.status.status != Status.RUNNING: - click.secho(f'{Symbols.ERROR} Deployment `{deployment_name}` is not running', fg=Colors.RED) + click.secho( + f"{Symbols.ERROR} Deployment `{deployment_name}` is not running", + fg=Colors.RED, + ) raise SystemExit(1) - if deployment.spec.runtime != 'device': - click.secho(f'Only device runtime is supported.', fg=Colors.RED) + if deployment.spec.runtime != "device": + click.secho("Only device runtime is supported.", fg=Colors.RED) raise SystemExit(1) if exec_name is None: - package = client.get_package(deployment.metadata.depends.nameOrGUID, - query={"version": deployment.metadata.depends.version}) + package = client.get_package( + deployment.metadata.depends.nameOrGUID, + query={"version": deployment.metadata.depends.version}, + ) executables = [e.name for e in package.spec.executables] if len(executables) == 1: exec_name = executables[0] else: - exec_name = show_selection(executables, '\nSelect executable') + exec_name = show_selection(executables, "\nSelect executable") - with Spinner(text='Executing command `{}`...'.format(command)): + with Spinner(text="Executing command `{}`...".format(command)): response = run_on_device( user=user, shell=shell, @@ -106,7 +115,7 @@ def execute_command( background=False, deployment=deployment, exec_name=exec_name, - device_name=deployment.spec.device.depends.nameOrGUID + device_name=deployment.spec.device.depends.nameOrGUID, ) click.echo(response) except Exception as e: diff --git a/riocli/deployment/inspect.py b/riocli/deployment/inspect.py index 86dedea8..c8e93606 100644 --- a/riocli/deployment/inspect.py +++ b/riocli/deployment/inspect.py @@ -21,21 +21,26 @@ from riocli.constants import Colors from riocli.utils import inspect_with_format -DEPLOYMENT_GUID_PATTERN = r'(^dep-[a-z0-9]+$|^inst-[a-z0-9]+$)' +DEPLOYMENT_GUID_PATTERN = r"(^dep-[a-z0-9]+$|^inst-[a-z0-9]+$)" @click.command( - 'inspect', + "inspect", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--format', '-f', 'format_type', default='yaml', - type=click.Choice(['json', 'yaml'], case_sensitive=False)) -@click.argument('deployment-name') +@click.option( + "--format", + "-f", + "format_type", + default="yaml", + type=click.Choice(["json", "yaml"], case_sensitive=False), +) +@click.argument("deployment-name") def inspect_deployment( - format_type: str, - deployment_name: str, + format_type: str, + deployment_name: str, ) -> None: """Inspect the deployment resource @@ -47,14 +52,14 @@ def inspect_deployment( deployment_obj = None if re.fullmatch(DEPLOYMENT_GUID_PATTERN, deployment_name): - deployments = client.list_deployments(query={'guids': [deployment_name]}) + deployments = client.list_deployments(query={"guids": [deployment_name]}) if deployments: deployment_obj = deployments[0] else: deployment_obj = client.get_deployment(deployment_name) if not deployment_obj: - click.secho("deployment not found", fg='red') + click.secho("deployment not found", fg="red") raise SystemExit(1) inspect_with_format(unmunchify(deployment_obj), format_type) diff --git a/riocli/deployment/list.py b/riocli/deployment/list.py index f49c7dec..28693b46 100644 --- a/riocli/deployment/list.py +++ b/riocli/deployment/list.py @@ -23,42 +23,59 @@ from riocli.v2client.util import process_errors ALL_PHASES = [ - 'InProgress', - 'Provisioning', - 'Succeeded', - 'FailedToStart', - 'Stopped', + "InProgress", + "Provisioning", + "Succeeded", + "FailedToStart", + "Stopped", ] DEFAULT_PHASES = [ - 'InProgress', - 'Provisioning', - 'Succeeded', - 'FailedToStart', + "InProgress", + "Provisioning", + "Succeeded", + "FailedToStart", ] @click.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--device', prompt_required=False, default='', type=str, - help='Filter the Deployment list by Device name') -@click.option('--phase', prompt_required=False, multiple=True, - type=click.Choice(ALL_PHASES), - default=DEFAULT_PHASES, - help='Filter the Deployment list by Phases') -@click.option('--label', '-l', 'labels', multiple=True, type=click.STRING, - default=(), help='Filter the deployment list by labels') -@click.option('--wide', '-w', is_flag=True, default=False, - help='Print more details', type=bool) +@click.option( + "--device", + prompt_required=False, + default="", + type=str, + help="Filter the Deployment list by Device name", +) +@click.option( + "--phase", + prompt_required=False, + multiple=True, + type=click.Choice(ALL_PHASES), + default=DEFAULT_PHASES, + help="Filter the Deployment list by Phases", +) +@click.option( + "--label", + "-l", + "labels", + multiple=True, + type=click.STRING, + default=(), + help="Filter the deployment list by labels", +) +@click.option( + "--wide", "-w", is_flag=True, default=False, help="Print more details", type=bool +) def list_deployments( - device: str, - phase: typing.List[str], - labels: typing.List[str], - wide: bool = False, + device: str, + phase: typing.List[str], + labels: typing.List[str], + wide: bool = False, ) -> None: """List the deployments in the current project @@ -77,8 +94,8 @@ def list_deployments( $ rio deployment list --label key1=value1 --label key2=value2 """ query = { - 'phases': phase, - 'labelSelector': labels, + "phases": phase, + "labelSelector": labels, } try: @@ -92,34 +109,44 @@ def list_deployments( def display_deployment_list( - deployments: typing.List[Deployment], - show_header: bool = True, - wide: bool = False, + deployments: typing.List[Deployment], + show_header: bool = True, + wide: bool = False, ): headers = [] if show_header: - headers = ['Name', 'Package', 'Creation Time (UTC)', 'Phase', 'Status'] + headers = ["Name", "Package", "Creation Time (UTC)", "Phase", "Status"] if show_header and wide: - headers.extend(['Deployment ID', 'Stopped Time (UTC)']) + headers.extend(["Deployment ID", "Stopped Time (UTC)"]) data = [] for d in deployments: - package_name_version = f'{d.metadata.depends.nameOrGUID} ({d.metadata.depends.version})' - phase = d.get('status', {}).get('phase', '') + package_name_version = ( + f"{d.metadata.depends.nameOrGUID} ({d.metadata.depends.version})" + ) + phase = d.get("status", {}).get("phase", "") - status = '' + status = "" if d.status: - if d.status.get('error_codes'): - status = click.style(process_errors(d.status.error_codes, no_action=True), fg=Colors.RED) + if d.status.get("error_codes"): + status = click.style( + process_errors(d.status.error_codes, no_action=True), fg=Colors.RED + ) else: status = d.status.status - row = [d.metadata.name, package_name_version, d.metadata.createdAt, phase, status] + row = [ + d.metadata.name, + package_name_version, + d.metadata.createdAt, + phase, + status, + ] if wide: - row.extend([d.metadata.guid, d.metadata.get('deletedAt')]) + row.extend([d.metadata.guid, d.metadata.get("deletedAt")]) data.append(row) diff --git a/riocli/deployment/logs.py b/riocli/deployment/logs.py index 8044b215..2d74fcac 100644 --- a/riocli/deployment/logs.py +++ b/riocli/deployment/logs.py @@ -20,20 +20,22 @@ @click.command( - 'logs', + "logs", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--replica', 'replica', default=0, - help='Replica identifier of the deployment') -@click.option('--exec', 'exec_name', default=None, - help='Name of a executable in the component') -@click.argument('deployment-name', type=str) +@click.option( + "--replica", "replica", default=0, help="Replica identifier of the deployment" +) +@click.option( + "--exec", "exec_name", default=None, help="Name of a executable in the component" +) +@click.argument("deployment-name", type=str) def deployment_logs( - replica: int, - exec_name: str, - deployment_name: str, + replica: int, + exec_name: str, + deployment_name: str, ) -> None: """Stream live logs from cloud deployments. diff --git a/riocli/deployment/model.py b/riocli/deployment/model.py index 22a9c97b..4ebd4328 100644 --- a/riocli/deployment/model.py +++ b/riocli/deployment/model.py @@ -21,7 +21,11 @@ from riocli.exceptions import ResourceNotFound from riocli.model import Model from riocli.v2client import Client -from riocli.v2client.error import HttpAlreadyExistsError, HttpNotFoundError, RetriesExhausted +from riocli.v2client.error import ( + HttpAlreadyExistsError, + HttpNotFoundError, + RetriesExhausted, +) class Deployment(Model): @@ -30,7 +34,9 @@ def __init__(self, *args, **kwargs): self.update(*args, **kwargs) def apply(self, *args, **kwargs) -> ApplyResult: - hard_dependencies = [d.nameOrGUID for d in self.spec.get('depends', []) if d.get('wait', False)] + hard_dependencies = [ + d.nameOrGUID for d in self.spec.get("depends", []) if d.get("wait", False) + ] client = new_v2_client() @@ -57,21 +63,25 @@ def delete(self, *args, **kwargs): def wait_for_dependencies( - client: Client, - deployment_names: typing.List[str], - retry_count: int = 50, - retry_interval: int = 6, + client: Client, + deployment_names: typing.List[str], + retry_count: int = 50, + retry_interval: int = 6, ) -> None: """Waits until all deployment_names are in RUNNING state.""" for _ in range(retry_count): - deployments = client.list_deployments(query={ - 'names': deployment_names, - 'phases': ['InProgress', 'Provisioning', 'Succeeded'] - }) + deployments = client.list_deployments( + query={ + "names": deployment_names, + "phases": ["InProgress", "Provisioning", "Succeeded"], + } + ) if all(d.status.status == Status.RUNNING for d in deployments): return time.sleep(retry_interval) - raise RetriesExhausted(f'Retries exhausted waiting for dependencies: {deployment_names}') + raise RetriesExhausted( + f"Retries exhausted waiting for dependencies: {deployment_names}" + ) diff --git a/riocli/deployment/status.py b/riocli/deployment/status.py index 2261b084..b51747c0 100644 --- a/riocli/deployment/status.py +++ b/riocli/deployment/status.py @@ -19,12 +19,12 @@ @click.command( - 'status', + "status", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('deployment-name', type=str) +@click.argument("deployment-name", type=str) def status(deployment_name: str) -> None: """Print the current status of a deployment. diff --git a/riocli/deployment/update.py b/riocli/deployment/update.py index 16c6d6b7..674e2ba8 100644 --- a/riocli/deployment/update.py +++ b/riocli/deployment/update.py @@ -31,53 +31,77 @@ @click.command( - 'update', + "update", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, deprecated=True, ) -@click.option('--force', '-f', '--silent', is_flag=True, default=False, - help='Skip confirmation') -@click.option('-a', '--all', 'update_all', is_flag=True, default=False, - help='Deletes all deployments in the project') -@click.option('--workers', '-w', - help="number of parallel workers while running update deployment " - "command. defaults to 10.", type=int, default=10) -@click.argument('deployment-name-or-regex', type=str, default="") +@click.option( + "--force", "-f", "--silent", is_flag=True, default=False, help="Skip confirmation" +) +@click.option( + "-a", + "--all", + "update_all", + is_flag=True, + default=False, + help="Deletes all deployments in the project", +) +@click.option( + "--workers", + "-w", + help="number of parallel workers while running update deployment " + "command. defaults to 10.", + type=int, + default=10, +) +@click.argument("deployment-name-or-regex", type=str, default="") @with_spinner(text="Updating...") def update_deployment( - force: bool, - workers: int, - deployment_name_or_regex: str, - update_all: bool = False, - spinner: Yaspin = None, + force: bool, + workers: int, + deployment_name_or_regex: str, + update_all: bool = False, + spinner: Yaspin = None, ) -> None: """Use the restart command instead""" _update(force, workers, deployment_name_or_regex, update_all, spinner) @click.command( - 'restart', + "restart", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--force', '-f', '--silent', is_flag=True, default=False, - help='Skip confirmation') -@click.option('-a', '--all', 'update_all', is_flag=True, default=False, - help='Deletes all deployments in the project') -@click.option('--workers', '-w', - help="number of parallel workers while running update deployment " - "command. defaults to 10.", type=int, default=10) -@click.argument('deployment-name-or-regex', type=str, default="") +@click.option( + "--force", "-f", "--silent", is_flag=True, default=False, help="Skip confirmation" +) +@click.option( + "-a", + "--all", + "update_all", + is_flag=True, + default=False, + help="Deletes all deployments in the project", +) +@click.option( + "--workers", + "-w", + help="number of parallel workers while running update deployment " + "command. defaults to 10.", + type=int, + default=10, +) +@click.argument("deployment-name-or-regex", type=str, default="") @with_spinner(text="Updating...") def restart_deployment( - force: bool, - workers: int, - deployment_name_or_regex: str, - update_all: bool = False, - spinner: Yaspin = None, + force: bool, + workers: int, + deployment_name_or_regex: str, + update_all: bool = False, + spinner: Yaspin = None, ) -> None: """Restarts one or more deployments by name or regex. @@ -99,11 +123,11 @@ def restart_deployment( def _update( - force: bool, - workers: int, - deployment_name_or_regex: str, - update_all: bool = False, - spinner: Yaspin = None, + force: bool, + workers: int, + deployment_name_or_regex: str, + update_all: bool = False, + spinner: Yaspin = None, ) -> None: client = new_v2_client() if not (deployment_name_or_regex or update_all): @@ -112,11 +136,11 @@ def _update( return try: - deployments = fetch_deployments( - client, deployment_name_or_regex, update_all) + deployments = fetch_deployments(client, deployment_name_or_regex, update_all) except Exception as e: spinner.text = click.style( - 'Failed to update deployment(s): {}'.format(e), Colors.RED) + "Failed to update deployment(s): {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -128,18 +152,17 @@ def _update( with spinner.hidden(): print_deployments_for_confirmation(deployments) - spinner.write('') + spinner.write("") if not force: with spinner.hidden(): - click.confirm('Do you want to update above deployment(s)?', abort=True) - spinner.write('') + click.confirm("Do you want to update above deployment(s)?", abort=True) + spinner.write("") try: f = functools.partial(_apply_update, client) result = apply_func_with_result( - f=f, items=deployments, - workers=workers, key=lambda x: x[0] + f=f, items=deployments, workers=workers, key=lambda x: x[0] ) data, fg, statuses = [], Colors.GREEN, [] @@ -147,36 +170,35 @@ def _update( fg = Colors.GREEN if status else Colors.RED icon = Symbols.SUCCESS if status else Symbols.ERROR statuses.append(status) - data.append([ - click.style(name, fg), - click.style('{} {}'.format(icon, msg), fg) - ]) + data.append( + [click.style(name, fg), click.style("{} {}".format(icon, msg), fg)] + ) with spinner.hidden(): - tabulate_data(data, headers=['Name', 'Status']) + tabulate_data(data, headers=["Name", "Status"]) icon = Symbols.SUCCESS if all(statuses) else Symbols.WARNING fg = Colors.GREEN if all(statuses) else Colors.YELLOW text = "successfully" if all(statuses) else "partially" - spinner.write('') - spinner.text = click.style( - 'Deployment(s) updated {}.'.format(text), fg) + spinner.write("") + spinner.text = click.style("Deployment(s) updated {}.".format(text), fg) spinner.ok(click.style(icon, fg)) except Exception as e: spinner.text = click.style( - 'Failed to update deployment(s): {}'.format(e), Colors.RED) + "Failed to update deployment(s): {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e def _apply_update( - client: Client, - result: Queue, - deployment: Deployment, + client: Client, + result: Queue, + deployment: Deployment, ) -> None: try: client.update_deployment(deployment.metadata.name, deployment) - result.put((deployment.metadata.name, True, 'Restarted')) + result.put((deployment.metadata.name, True, "Restarted")) except Exception as e: result.put((deployment.metadata.name, False, str(e))) diff --git a/riocli/deployment/util.py b/riocli/deployment/util.py index 02165d89..c39d677b 100644 --- a/riocli/deployment/util.py +++ b/riocli/deployment/util.py @@ -30,29 +30,41 @@ def fetch_deployments( - client: Client, - deployment_name_or_regex: str, - include_all: bool, + client: Client, + deployment_name_or_regex: str, + include_all: bool, ) -> typing.List[munch.Munch]: - deployments = client.list_deployments(query={'phases': DEFAULT_PHASES}) + deployments = client.list_deployments(query={"phases": DEFAULT_PHASES}) result = [] for deployment in deployments: - if (include_all or deployment_name_or_regex == deployment.metadata.name or - deployment_name_or_regex == deployment.metadata.guid or - (deployment_name_or_regex not in deployment.metadata.name and - re.search(r'^{}$'.format(deployment_name_or_regex), deployment.metadata.name))): + if ( + include_all + or deployment_name_or_regex == deployment.metadata.name + or deployment_name_or_regex == deployment.metadata.guid + or ( + deployment_name_or_regex not in deployment.metadata.name + and re.search( + r"^{}$".format(deployment_name_or_regex), deployment.metadata.name + ) + ) + ): result.append(deployment) return result def print_deployments_for_confirmation(deployments: typing.List[munch.Munch]): - headers = ['Name', 'GUID', 'Phase', 'Status'] + headers = ["Name", "GUID", "Phase", "Status"] data = [] for deployment in deployments: data.append( - [deployment.metadata.name, deployment.metadata.guid, deployment.status.phase, - deployment.status.status]) + [ + deployment.metadata.name, + deployment.metadata.guid, + deployment.status.phase, + deployment.status.status, + ] + ) tabulate_data(data, headers) diff --git a/riocli/deployment/wait.py b/riocli/deployment/wait.py index 4425795d..6484ea7b 100644 --- a/riocli/deployment/wait.py +++ b/riocli/deployment/wait.py @@ -21,16 +21,16 @@ @click.command( - 'wait', + "wait", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('deployment-name', type=str) +@click.argument("deployment-name", type=str) @with_spinner(text="Waiting for deployment...", timer=True) def wait_for_deployment( - deployment_name: str, - spinner=None, + deployment_name: str, + spinner=None, ) -> None: """Wait until the deployment succeeds/fails @@ -40,11 +40,14 @@ def wait_for_deployment( try: client = new_v2_client() deployment = client.poll_deployment(deployment_name) - spinner.text = click.style('Phase: Succeeded Status: {}'.format(deployment.status.status), fg=Colors.GREEN) + spinner.text = click.style( + "Phase: Succeeded Status: {}".format(deployment.status.status), + fg=Colors.GREEN, + ) spinner.green.ok(Symbols.SUCCESS) except RetriesExhausted as e: spinner.write(click.style(str(e), fg=Colors.RED)) - spinner.text = click.style('Try again', Colors.RED) + spinner.text = click.style("Try again", Colors.RED) spinner.red.fail(Symbols.ERROR) except DeploymentNotRunning as e: spinner.text = click.style(str(e), fg=Colors.RED) diff --git a/riocli/device/__init__.py b/riocli/device/__init__.py index b111eabb..fc8ac999 100644 --- a/riocli/device/__init__.py +++ b/riocli/device/__init__.py @@ -33,8 +33,8 @@ @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color="yellow", + help_options_color="green", ) def device(): """ diff --git a/riocli/device/config.py b/riocli/device/config.py index 7bb95901..f6bacb62 100644 --- a/riocli/device/config.py +++ b/riocli/device/config.py @@ -26,7 +26,7 @@ @click.group( - 'config', + "config", invoke_without_command=False, cls=HelpColorsGroup, help_headers_color=Colors.YELLOW, @@ -40,12 +40,12 @@ def device_config() -> None: @device_config.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('device-name', type=str) +@click.argument("device-name", type=str) @name_to_guid def list_config(device_name: str, device_guid: str) -> None: """ @@ -57,27 +57,27 @@ def list_config(device_name: str, device_guid: str) -> None: config_variables = device.get_config_variables() _display_config_list(config_variables, show_header=True) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) @device_config.command( - 'create', + "create", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('device-name', type=str) -@click.argument('key', type=str) -@click.argument('value', type=str) +@click.argument("device-name", type=str) +@click.argument("key", type=str) +@click.argument("value", type=str) @name_to_guid -@with_spinner(text='Creating new config variable...') +@with_spinner(text="Creating new config variable...") def create_config( - device_name: str, - device_guid: str, - key: str, - value: str, - spinner=None, + device_name: str, + device_guid: str, + key: str, + value: str, + spinner=None, ) -> None: """ Create a new config variable on the device @@ -86,73 +86,85 @@ def create_config( client = new_client() device = client.get_device(device_id=device_guid) device.add_config_variable(key, value) - spinner.text = click.style('Config variable added successfully.', fg=Colors.GREEN) + spinner.text = click.style("Config variable added successfully.", fg=Colors.GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style('Failed to add config variable: {}'.format(e), fg=Colors.RED) + spinner.text = click.style( + "Failed to add config variable: {}".format(e), fg=Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @device_config.command( - 'update', + "update", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('device-name', type=str) -@click.argument('key', type=str) -@click.argument('value', type=str) +@click.argument("device-name", type=str) +@click.argument("key", type=str) +@click.argument("value", type=str) @name_to_guid -@with_spinner(text='Updating config variable...') +@with_spinner(text="Updating config variable...") def update_config( - device_name: str, - device_guid: str, - key: str, - value: str, - spinner=None, + device_name: str, + device_guid: str, + key: str, + value: str, + spinner=None, ) -> None: """ Update the config variable on the device """ try: _update_config_variable(device_guid, key, value) - spinner.text = click.style('Config variable updated successfully.', fg=Colors.GREEN) + spinner.text = click.style( + "Config variable updated successfully.", fg=Colors.GREEN + ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style('Failed to update config variable: {}'.format(e), fg=Colors.RED) + spinner.text = click.style( + "Failed to update config variable: {}".format(e), fg=Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @device_config.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('device-name', type=str) -@click.argument('key', type=str) +@click.argument("device-name", type=str) +@click.argument("key", type=str) @name_to_guid -@with_spinner(text='Deleting config variable...') +@with_spinner(text="Deleting config variable...") def delete_config(device_name: str, device_guid: str, key: str, spinner=None) -> None: """ Delete the config variable on the device """ try: _delete_config_variable(device_guid, key) - spinner.text = click.style('Config variable deleted successfully.', fg=Colors.GREEN) + spinner.text = click.style( + "Config variable deleted successfully.", fg=Colors.GREEN + ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style('Failed to delete config variable: {}'.format(e), fg=Colors.RED) + spinner.text = click.style( + "Failed to delete config variable: {}".format(e), fg=Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e -def _display_config_list(config_variables: typing.List[DeviceConfig], show_header: bool = True) -> None: +def _display_config_list( + config_variables: typing.List[DeviceConfig], show_header: bool = True +) -> None: headers = [] if show_header: - headers = ('Key', 'Value') + headers = ("Key", "Value") data = [[c.key, c.value] for c in config_variables] @@ -174,9 +186,11 @@ def _delete_config_variable(device_guid: str, key: str) -> None: device.delete_config_variable(config_variable.id) -def _find_config_variable(config_variables: typing.List[DeviceConfig], key: str) -> DeviceConfig: +def _find_config_variable( + config_variables: typing.List[DeviceConfig], key: str +) -> DeviceConfig: for cfg in config_variables: if cfg.key == key: return cfg - raise Exception('Config variable not found') + raise Exception("Config variable not found") diff --git a/riocli/device/create.py b/riocli/device/create.py index 4b2cde38..a9116af3 100644 --- a/riocli/device/create.py +++ b/riocli/device/create.py @@ -19,27 +19,46 @@ from riocli.config import new_client -@click.command('create', hidden=True) -@click.option('--description', type=str, help='Description of the device', default='') -@click.option('--runtime', help='Runtime of the Device', multiple=True, - type=click.Choice(['preinstalled', 'dockercompose'], case_sensitive=False)) -@click.option('--ros', help='ROS Distribution for the Device', default='melodic', - type=click.Choice(['kinetic', 'melodic', 'noetic'], case_sensitive=False)) -@click.option('--python', help='Python Version to use on the Device', default='3', - type=click.Choice(['2', '3'], case_sensitive=False)) -@click.option('--rosbag-mount-path', type=str, default='/opt/rapyuta/volumes/rosbag', - help='Path to store recorded ROSBags (only dockercompose)') -@click.option('--catkin-workspace', default='/home/rapyuta/catkin_ws', - help='Path to the Catkin Workspace (only preinstalled)') -@click.argument('device-name', type=str) +@click.command("create", hidden=True) +@click.option("--description", type=str, help="Description of the device", default="") +@click.option( + "--runtime", + help="Runtime of the Device", + multiple=True, + type=click.Choice(["preinstalled", "dockercompose"], case_sensitive=False), +) +@click.option( + "--ros", + help="ROS Distribution for the Device", + default="melodic", + type=click.Choice(["kinetic", "melodic", "noetic"], case_sensitive=False), +) +@click.option( + "--python", + help="Python Version to use on the Device", + default="3", + type=click.Choice(["2", "3"], case_sensitive=False), +) +@click.option( + "--rosbag-mount-path", + type=str, + default="/opt/rapyuta/volumes/rosbag", + help="Path to store recorded ROSBags (only dockercompose)", +) +@click.option( + "--catkin-workspace", + default="/home/rapyuta/catkin_ws", + help="Path to the Catkin Workspace (only preinstalled)", +) +@click.argument("device-name", type=str) def create_device( - device_name: str, - description: str, - runtime: [], - ros: str, - python: str, - rosbag_mount_path: str, - catkin_workspace: str, + device_name: str, + description: str, + runtime: [], + ros: str, + python: str, + rosbag_mount_path: str, + catkin_workspace: str, ) -> None: """ Create a new device on the Platform @@ -51,12 +70,18 @@ def create_device( ros_distro = ROSDistro(ros) runtime_docker = DeviceRuntime.DOCKER in runtime runtime_preinstalled = DeviceRuntime.PREINSTALLED in runtime - device = Device(name=device_name, description=description, ros_distro=ros_distro, - runtime_docker=runtime_docker, runtime_preinstalled=runtime_preinstalled, - python_version=python_version, rosbag_mount_path=rosbag_mount_path, - ros_workspace=catkin_workspace) + device = Device( + name=device_name, + description=description, + ros_distro=ros_distro, + runtime_docker=runtime_docker, + runtime_preinstalled=runtime_preinstalled, + python_version=python_version, + rosbag_mount_path=rosbag_mount_path, + ros_workspace=catkin_workspace, + ) client.create_device(device) - click.secho('Device created successfully!', fg='green') + click.secho("Device created successfully!", fg="green") except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) diff --git a/riocli/device/delete.py b/riocli/device/delete.py index 67579c4a..a5cd6433 100644 --- a/riocli/device/delete.py +++ b/riocli/device/delete.py @@ -30,25 +30,32 @@ @click.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--force', '-f', '--silent', is_flag=True, default=False, - help='Skip confirmation') -@click.option('-a', '--all', 'delete_all', is_flag=True, default=False, - help='Delete all devices') -@click.option('--workers', '-w', - help="Number of parallel workers for deleting devices. Defaults to 10.", type=int, default=10) -@click.argument('device-name-or-regex', type=str, default="") -@with_spinner(text='Deleting device...') +@click.option( + "--force", "-f", "--silent", is_flag=True, default=False, help="Skip confirmation" +) +@click.option( + "-a", "--all", "delete_all", is_flag=True, default=False, help="Delete all devices" +) +@click.option( + "--workers", + "-w", + help="Number of parallel workers for deleting devices. Defaults to 10.", + type=int, + default=10, +) +@click.argument("device-name-or-regex", type=str, default="") +@with_spinner(text="Deleting device...") def delete_device( - force: bool, - workers: int, - device_name_or_regex: str, - delete_all: bool = False, - spinner: Yaspin = None, + force: bool, + workers: int, + device_name_or_regex: str, + delete_all: bool = False, + spinner: Yaspin = None, ) -> None: """Delete one or more devices with a name or a regex pattern. @@ -81,16 +88,14 @@ def delete_device( """ client = new_client() if not (device_name_or_regex or delete_all): - spinner.text = 'Nothing to delete' + spinner.text = "Nothing to delete" spinner.green.ok(Symbols.SUCCESS) return try: - devices = fetch_devices( - client, device_name_or_regex, delete_all) + devices = fetch_devices(client, device_name_or_regex, delete_all) except Exception as e: - spinner.text = click.style( - 'Failed to delete device(s): {}'.format(e), Colors.RED) + spinner.text = click.style("Failed to delete device(s): {}".format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -99,27 +104,26 @@ def delete_device( spinner.red.fail(Symbols.ERROR) raise SystemExit(1) - headers = ['Name', 'Device ID', 'Status'] + headers = ["Name", "Device ID", "Status"] data = [[d.name, d.uuid, d.status] for d in devices] with spinner.hidden(): tabulate_data(data, headers) - spinner.write('') + spinner.write("") if not force: with spinner.hidden(): - click.confirm('Do you want to delete above device(s)?', abort=True) - spinner.write('') + click.confirm("Do you want to delete above device(s)?", abort=True) + spinner.write("") try: f = functools.partial(_delete_deivce, client) result = apply_func_with_result( - f=f, items=devices, - workers=workers, key=lambda x: x[0] + f=f, items=devices, workers=workers, key=lambda x: x[0] ) - data, fg, statuses = [], Colors.GREEN, [] + data, fg = [], Colors.GREEN success_count, failed_count = 0, 0 for name, response in result: @@ -127,48 +131,49 @@ def delete_device( fg = Colors.GREEN icon = Symbols.SUCCESS success_count += 1 - msg = '' + msg = "" else: fg = Colors.RED icon = Symbols.ERROR failed_count += 1 msg = get_error_message(response, name) - data.append([ - click.style(name, fg), - click.style(icon, fg), - click.style(msg, fg) - ]) + data.append( + [click.style(name, fg), click.style(icon, fg), click.style(msg, fg)] + ) with spinner.hidden(): - tabulate_data(data, headers=['Name', 'Status', 'Message']) + tabulate_data(data, headers=["Name", "Status", "Message"]) - spinner.write('') + spinner.write("") if failed_count == 0 and success_count == len(devices): - spinner_text = click.style('{} device(s) deleted successfully.'.format(len(devices)), Colors.GREEN) + spinner_text = click.style( + "{} device(s) deleted successfully.".format(len(devices)), Colors.GREEN + ) spinner_char = click.style(Symbols.SUCCESS, Colors.GREEN) elif success_count == 0 and failed_count == len(devices): - spinner_text = click.style('Failed to delete devices', Colors.YELLOW) + spinner_text = click.style("Failed to delete devices", Colors.YELLOW) spinner_char = click.style(Symbols.WARNING, Colors.YELLOW) else: spinner_text = click.style( - '{}/{} devices deleted successfully'.format(success_count, len(devices)), Colors.YELLOW) + "{}/{} devices deleted successfully".format(success_count, len(devices)), + Colors.YELLOW, + ) spinner_char = click.style(Symbols.WARNING, Colors.YELLOW) spinner.text = spinner_text spinner.ok(spinner_char) except Exception as e: - spinner.text = click.style( - 'Failed to delete devices: {}'.format(e), Colors.RED) + spinner.text = click.style("Failed to delete devices: {}".format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e def _delete_deivce( - client: Client, - result: Queue, - device: Device = None, + client: Client, + result: Queue, + device: Device = None, ) -> None: response = requests.models.Response() try: @@ -181,9 +186,9 @@ def _delete_deivce( def get_error_message(response: requests.models.Response, name: str) -> str: if response.status_code: r = response.json() - error = r.get('response', {}).get('error') + error = r.get("response", {}).get("error") - if 'deployments' in error: - return 'Device {0} has running deployments.'.format(name) + if "deployments" in error: + return "Device {0} has running deployments.".format(name) return "" diff --git a/riocli/device/deployment.py b/riocli/device/deployment.py index 53ddffc2..db6d70cc 100644 --- a/riocli/device/deployment.py +++ b/riocli/device/deployment.py @@ -20,26 +20,28 @@ from riocli.device.util import name_to_guid DEFAULT_PHASES = [ - 'InProgress', - 'Provisioning', - 'Succeeded', - 'FailedToStart', + "InProgress", + "Provisioning", + "Succeeded", + "FailedToStart", ] @click.command( - 'deployments', + "deployments", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('device-name', type=str) +@click.argument("device-name", type=str) @name_to_guid def list_deployments(device_name: str, device_guid: str) -> None: """Lists all the deployments running on the device.""" try: client = new_v2_client() - deployments = client.list_deployments(query={'deviceName': device_name, 'phases': DEFAULT_PHASES}) + deployments = client.list_deployments( + query={"deviceName": device_name, "phases": DEFAULT_PHASES} + ) display_deployment_list(deployments, show_header=True) except Exception as e: click.secho(str(e), fg=Colors.RED) diff --git a/riocli/device/execute.py b/riocli/device/execute.py index 4fe3ce15..1768fd71 100644 --- a/riocli/device/execute.py +++ b/riocli/device/execute.py @@ -22,22 +22,18 @@ @click.command( - 'execute', + "execute", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--user', default='root') -@click.option('--shell', default='/bin/bash') -@click.argument('device-name', type=str) -@click.argument('command', nargs=-1) +@click.option("--user", default="root") +@click.option("--shell", default="/bin/bash") +@click.argument("device-name", type=str) +@click.argument("command", nargs=-1) @name_to_guid def execute_command( - device_name: str, - device_guid: str, - user: str, - shell: str, - command: typing.List[str] + device_name: str, device_guid: str, user: str, shell: str, command: typing.List[str] ) -> None: """Execute commands on a device. diff --git a/riocli/device/files.py b/riocli/device/files.py index 9c9a31e8..196b7b46 100644 --- a/riocli/device/files.py +++ b/riocli/device/files.py @@ -25,7 +25,7 @@ @click.group( - 'uploads', + "uploads", invoke_without_command=False, cls=HelpColorsGroup, help_headers_color=Colors.YELLOW, @@ -40,11 +40,13 @@ def device_uploads() -> None: pass -@device_uploads.command('list', - cls=HelpColorsCommand, - help_headers_color=Colors.YELLOW, - help_options_color=Colors.GREEN, ) -@click.argument('device-name', type=str) +@device_uploads.command( + "list", + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.argument("device-name", type=str) @name_to_guid def list_uploads(device_name: str, device_guid: str) -> None: """List all files uploaded from a device. @@ -63,29 +65,40 @@ def list_uploads(device_name: str, device_guid: str) -> None: @device_uploads.command( - 'create', + "create", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--max-upload-rate', type=int, default=1 * 1024 * 1024, - help='Network bandwidth limit to be used for upload (Bytes per second)') -@click.option('--override', is_flag=True, default=False, help='Flag to override destination file') -@click.option('--purge', is_flag=True, default=False, help='Flag to enable purging the file, after it is uploaded') -@click.argument('device-name', type=str) -@click.argument('upload-name', type=str) -@click.argument('file-path', type=str) +@click.option( + "--max-upload-rate", + type=int, + default=1 * 1024 * 1024, + help="Network bandwidth limit to be used for upload (Bytes per second)", +) +@click.option( + "--override", is_flag=True, default=False, help="Flag to override destination file" +) +@click.option( + "--purge", + is_flag=True, + default=False, + help="Flag to enable purging the file, after it is uploaded", +) +@click.argument("device-name", type=str) +@click.argument("upload-name", type=str) +@click.argument("file-path", type=str) @name_to_guid -@with_spinner(text='Uploading...') +@with_spinner(text="Uploading...") def create_upload( - device_name: str, - device_guid: str, - upload_name: str, - file_path: str, - max_upload_rate: int, - override: bool, - purge: bool, - spinner=None, + device_name: str, + device_guid: str, + upload_name: str, + file_path: str, + max_upload_rate: int, + override: bool, + purge: bool, + spinner=None, ) -> None: """Upload a file from a device to the cloud. @@ -123,32 +136,34 @@ def create_upload( file_name=upload_name, max_upload_rate=max_upload_rate, override=override, - purge_after=purge + purge_after=purge, ) device.upload_log_file(upload_request) - spinner.text = click.style('File upload requested successfully.', fg=Colors.GREEN) + spinner.text = click.style("File upload requested successfully.", fg=Colors.GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style('Failed to request upload: {}'.format(e), fg=Colors.RED) + spinner.text = click.style( + "Failed to request upload: {}".format(e), fg=Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @device_uploads.command( - 'status', + "status", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('device-name', type=str) -@click.argument('file-name', type=str) +@click.argument("device-name", type=str) +@click.argument("file-name", type=str) @name_to_guid @name_to_request_id def upload_status( - device_name: str, - device_guid: str, - file_name: str, - request_id: str, + device_name: str, + device_guid: str, + file_name: str, + request_id: str, ) -> None: """Check the status of a file upload.""" try: @@ -162,53 +177,45 @@ def upload_status( @device_uploads.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('device-name', type=str) -@click.argument('file-name', type=str) +@click.argument("device-name", type=str) +@click.argument("file-name", type=str) @name_to_guid @name_to_request_id -@with_spinner(text='Deleting upload...') +@with_spinner(text="Deleting upload...") def delete_upload( - device_name: str, - device_guid: str, - file_name: str, - request_id: str, - spinner=None + device_name: str, device_guid: str, file_name: str, request_id: str, spinner=None ) -> None: """Delete an uploaded file.""" try: client = new_client() device = client.get_device(device_id=device_guid) device.delete_uploaded_log_file(request_uuid=request_id) - spinner.text = click.style('Deleted upload successfully.', fg=Colors.GREEN) + spinner.text = click.style("Deleted upload successfully.", fg=Colors.GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style('Failed to delete upload: {}'.format(e), fg=Colors.RED) + spinner.text = click.style("Failed to delete upload: {}".format(e), fg=Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @device_uploads.command( - 'download', + "download", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('device-name', type=str) -@click.argument('file-name', type=str) +@click.argument("device-name", type=str) +@click.argument("file-name", type=str) @name_to_guid @name_to_request_id -@with_spinner(text='Downloading file...') +@with_spinner(text="Downloading file...") def download_log( - device_name: str, - device_guid: str, - file_name: str, - request_id: str, - spinner=None + device_name: str, device_guid: str, file_name: str, request_id: str, spinner=None ) -> None: """Download a file from the device.""" try: @@ -224,29 +231,25 @@ def download_log( @device_uploads.command( - 'cancel', + "cancel", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('device-name', type=str) -@click.argument('file-name', type=str) +@click.argument("device-name", type=str) +@click.argument("file-name", type=str) @name_to_guid @name_to_request_id -@with_spinner(text='Cancelling upload...') +@with_spinner(text="Cancelling upload...") def cancel_upload( - device_name: str, - device_guid: str, - file_name: str, - request_id: str, - spinner=None + device_name: str, device_guid: str, file_name: str, request_id: str, spinner=None ) -> None: """Cancel an ongoing upload operation.""" try: client = new_client() device = client.get_device(device_id=device_guid) device.cancel_log_file_upload(request_uuid=request_id) - spinner.text = click.style('Cancelled upload.', fg=Colors.GREEN) + spinner.text = click.style("Cancelled upload.", fg=Colors.GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: spinner.text = click.style(str(e), fg=Colors.RED) @@ -255,24 +258,28 @@ def cancel_upload( @device_uploads.command( - 'share', + "share", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--expiry', help='Flag to set the expiry date for the Shared URL [default: 7 days]', default=7) -@click.argument('device-name', type=str) -@click.argument('file-name', type=str) +@click.option( + "--expiry", + help="Flag to set the expiry date for the Shared URL [default: 7 days]", + default=7, +) +@click.argument("device-name", type=str) +@click.argument("file-name", type=str) @name_to_guid @name_to_request_id -@with_spinner(text='Creating shared URL...') +@with_spinner(text="Creating shared URL...") def shared_url( - device_name: str, - device_guid: str, - file_name: str, - request_id: str, - expiry: int, - spinner=None + device_name: str, + device_guid: str, + file_name: str, + request_id: str, + expiry: int, + spinner=None, ) -> None: """Share a URL for an uploaded file. @@ -293,11 +300,15 @@ def shared_url( client = new_client() device = client.get_device(device_id=device_guid) expiry_time = datetime.now() + timedelta(days=expiry) - public_url = device.create_shared_url(SharedURL(request_id, expiry_time=expiry_time)) + public_url = device.create_shared_url( + SharedURL(request_id, expiry_time=expiry_time) + ) spinner.text = click.style(public_url.url, fg=Colors.GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style('Failed to create shared URL: {}'.format(e), fg=Colors.RED) + spinner.text = click.style( + "Failed to create shared URL: {}".format(e), fg=Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -305,7 +316,7 @@ def shared_url( def _display_upload_list(uploads: LogUploads, show_header: bool = True) -> None: headers = [] if show_header: - headers = ('Upload ID', 'Name', 'Status', 'Total Size') + headers = ("Upload ID", "Name", "Status", "Total Size") data = [[u.request_uuid, u.filename, u.status, u.total_size] for u in uploads] diff --git a/riocli/device/inspect.py b/riocli/device/inspect.py index a37e1557..32c6f87a 100644 --- a/riocli/device/inspect.py +++ b/riocli/device/inspect.py @@ -22,14 +22,19 @@ @click.command( - 'inspect', + "inspect", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--format', '-f', 'format_type', - type=click.Choice(['json', 'yaml'], case_sensitive=False), default='yaml') -@click.argument('device-name', type=str) +@click.option( + "--format", + "-f", + "format_type", + type=click.Choice(["json", "yaml"], case_sensitive=False), + default="yaml", +) +@click.argument("device-name", type=str) @name_to_guid def inspect_device(format_type: str, device_name: str, device_guid: str) -> None: """Print the details of a device. @@ -49,7 +54,7 @@ def inspect_device(format_type: str, device_name: str, device_guid: str) -> None def make_device_inspectable(device: Device) -> dict: data = {} for key, val in device.items(): - if key.startswith('_') or key in ['deviceId']: + if key.startswith("_") or key in ["deviceId"]: continue data[key] = val diff --git a/riocli/device/label.py b/riocli/device/label.py index cbe25936..90030442 100644 --- a/riocli/device/label.py +++ b/riocli/device/label.py @@ -25,10 +25,10 @@ @click.group( - 'labels', + "labels", invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', + help_headers_color="yellow", help_options_color=Colors.GREEN, ) def device_labels() -> None: @@ -39,8 +39,8 @@ def device_labels() -> None: pass -@device_labels.command('list') -@click.argument('device-name', type=str) +@device_labels.command("list") +@click.argument("device-name", type=str) @name_to_guid def list_labels(device_name: str, device_guid: str) -> None: """List all the labels for a device.""" @@ -51,13 +51,13 @@ def list_labels(device_name: str, device_guid: str) -> None: _display_label_list(labels, show_header=True) except Exception as e: click.secho(str(e), fg=Colors.RED) - raise SystemExit(1) + raise SystemExit(1) from e -@device_labels.command('create') -@click.argument('device-name', type=str) -@click.argument('key', type=str) -@click.argument('value', type=str) +@device_labels.command("create") +@click.argument("device-name", type=str) +@click.argument("key", type=str) +@click.argument("value", type=str) @name_to_guid def create_label(device_name: str, device_guid: str, key: str, value: str) -> None: """Create a new label on a device""" @@ -66,38 +66,38 @@ def create_label(device_name: str, device_guid: str, key: str, value: str) -> No client = new_client() device = client.get_device(device_id=device_guid) device.add_label(key, value) - click.secho('Label added successfully!', fg=Colors.GREEN) + click.secho("Label added successfully!", fg=Colors.GREEN) except Exception as e: click.secho(str(e), fg=Colors.RED) raise SystemExit(1) -@device_labels.command('update') -@click.argument('device-name', type=str) -@click.argument('key', type=str) -@click.argument('value', type=str) +@device_labels.command("update") +@click.argument("device-name", type=str) +@click.argument("key", type=str) +@click.argument("value", type=str) @name_to_guid def update_label(device_name: str, device_guid: str, key: str, value: str) -> None: """Update a label on a device""" try: with spinner(): _update_label(device_guid, key, value) - click.secho('Label updated successfully!', fg=Colors.GREEN) + click.secho("Label updated successfully!", fg=Colors.GREEN) except Exception as e: click.secho(str(e), fg=Colors.RED) raise SystemExit(1) -@device_labels.command('delete') -@click.argument('device-name', type=str) -@click.argument('key', type=str) +@device_labels.command("delete") +@click.argument("device-name", type=str) +@click.argument("key", type=str) @name_to_guid def delete_label(device_name: str, device_guid: str, key: str) -> None: """Delete a label on a device.""" try: with spinner(): _delete_label(device_guid, key) - click.secho('Label deleted successfully!', fg=Colors.GREEN) + click.secho("Label deleted successfully!", fg=Colors.GREEN) except Exception as e: click.secho(str(e), fg=Colors.RED) raise SystemExit(1) @@ -106,7 +106,7 @@ def delete_label(device_name: str, device_guid: str, key: str) -> None: def _display_label_list(labels: typing.List[Label], show_header: bool = True) -> None: headers = [] if show_header: - headers = ('Key', 'Value') + headers = ("Key", "Value") data = [[l.key, l.value] for l in labels] @@ -133,4 +133,4 @@ def _find_label(labels: typing.List[Label], key: str) -> Label: if label.key == key: return label - raise Exception('Label not found') + raise Exception("Label not found") diff --git a/riocli/device/list.py b/riocli/device/list.py index bb3ad384..2b0ee8c3 100644 --- a/riocli/device/list.py +++ b/riocli/device/list.py @@ -23,7 +23,7 @@ @click.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, @@ -43,7 +43,7 @@ def list_devices() -> None: def _display_device_list(devices: typing.List[Device], show_header: bool = True) -> None: headers = [] if show_header: - headers = ('Device ID', 'Name', 'Status') + headers = ("Device ID", "Name", "Status") data = [[d.uuid, d.name, d.status] for d in devices] diff --git a/riocli/device/migrate.py b/riocli/device/migrate.py index 20b28f9c..fffc1602 100644 --- a/riocli/device/migrate.py +++ b/riocli/device/migrate.py @@ -23,27 +23,41 @@ @click.command( - 'migrate', + "migrate", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, - help_options_color=Colors.GREEN + help_options_color=Colors.GREEN, +) +@click.argument("device-name", type=str) +@click.argument("project-name", type=str) +@click.option( + "--enable-vpn", + is_flag=True, + type=click.BOOL, + default=False, + help="Enable VPN after migrating to the destination project.", +) +@click.option( + "--advertise-routes", + is_flag=True, + type=click.BOOL, + default=False, + help="Advertise subnets configured in project to VPN peers", ) -@click.argument('device-name', type=str) -@click.argument('project-name', type=str) -@click.option('--enable-vpn', is_flag=True, - type=click.BOOL, default=False, - help="Enable VPN after migrating to the destination project.") -@click.option('--advertise-routes', is_flag=True, - type=click.BOOL, default=False, - help="Advertise subnets configured in project to VPN peers") @name_to_guid @project_name_to_guid @click.pass_context @with_spinner(text="Migrating device...") -def migrate_project(ctx: click.Context, device_name: str, device_guid: str, - project_name: str, project_guid: str, - enable_vpn: bool, advertise_routes: bool, - spinner: Yaspin) -> None: +def migrate_project( + ctx: click.Context, + device_name: str, + device_guid: str, + project_name: str, + project_guid: str, + enable_vpn: bool, + advertise_routes: bool, + spinner: Yaspin, +) -> None: """Migrate a device from current project to the target project. This process may take some time since it involves multiple steps. @@ -58,21 +72,30 @@ def migrate_project(ctx: click.Context, device_name: str, device_guid: str, try: migrate_device_to_project(ctx, device_guid, project_guid) spinner.write( - click.style('{} Device {} migrated successfully.'.format(Symbols.SUCCESS, device_name), - fg=Colors.GREEN)) + click.style( + "{} Device {} migrated successfully.".format( + Symbols.SUCCESS, device_name + ), + fg=Colors.GREEN, + ) + ) if enable_vpn: - spinner.text = 'Enabling VPN on device...' + spinner.text = "Enabling VPN on device..." client = new_client() client.set_project(project_guid) client.toggle_features( device_id=device_guid, - features=[('vpn', True)], - config={'vpn': {'advertise_routes': advertise_routes}}, + features=[("vpn", True)], + config={"vpn": {"advertise_routes": advertise_routes}}, + ) + spinner.write( + click.style( + "{} Enabled VPN on the device.".format(Symbols.SUCCESS), + fg=Colors.GREEN, + ) ) - spinner.write(click.style('{} Enabled VPN on the device.'.format(Symbols.SUCCESS), fg=Colors.GREEN)) except Exception as e: - spinner.text = click.style( - 'Failed to migrate device: {}'.format(e), Colors.RED) + spinner.text = click.style("Failed to migrate device: {}".format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e diff --git a/riocli/device/model.py b/riocli/device/model.py index 01701225..ed077f86 100644 --- a/riocli/device/model.py +++ b/riocli/device/model.py @@ -16,8 +16,15 @@ from riocli.config import new_client from riocli.constants import ApplyResult -from riocli.device.util import (DeviceNotFound, create_hwil_device, delete_hwil_device, execute_onboard_command, - find_device_by_name, make_device_labels_from_hwil_device, wait_until_online) +from riocli.device.util import ( + DeviceNotFound, + create_hwil_device, + delete_hwil_device, + execute_onboard_command, + find_device_by_name, + make_device_labels_from_hwil_device, + wait_until_online, +) from riocli.exceptions import ResourceNotFound from riocli.model import Model @@ -37,10 +44,10 @@ def apply(self, *args, **kwargs) -> ApplyResult: except DeviceNotFound: pass - virtual = self.spec.get('virtual', {}) + virtual = self.spec.get("virtual", {}) # If the device is not virtual, create it and return. - if not virtual.get('enabled', False): + if not virtual.get("enabled", False): if device is None: client.create_device(self.to_v1()) return ApplyResult.CREATED @@ -48,7 +55,7 @@ def apply(self, *args, **kwargs) -> ApplyResult: return ApplyResult.EXISTS # Return if the device is already online or initializing. - if device and device['status'] in ('ONLINE', 'INITIALIZING'): + if device and device["status"] in ("ONLINE", "INITIALIZING"): return ApplyResult.EXISTS result = ApplyResult.CREATED if device is None else ApplyResult.UPDATED @@ -61,17 +68,17 @@ def apply(self, *args, **kwargs) -> ApplyResult: # Create the rapyuta.io device with labels if the # device does not exist. Else, update the labels. if device is None: - labels.update(self.metadata.get('labels', {})) + labels.update(self.metadata.get("labels", {})) self.metadata.labels = labels device = client.create_device(self.to_v1()) else: - device_labels = device.get('labels', {}) + device_labels = device.get("labels", {}) # Convert list to dict for easy access. - device_labels = {l['key']: l for l in device_labels} + device_labels = {l["key"]: l for l in device_labels} # Add or update labels in the device. for k, v in labels.items(): if k in device_labels: - device_labels[k]['value'] = v + device_labels[k]["value"] = v device.update_label(device_labels[k]) continue @@ -82,7 +89,7 @@ def apply(self, *args, **kwargs) -> ApplyResult: onboard_command = onboard_script.full_command() execute_onboard_command(hwil_response.id, onboard_command) - if virtual.get('wait', False): + if virtual.get("wait", False): wait_until_online(device) return result @@ -95,7 +102,7 @@ def delete(self, *args, **kwargs) -> None: except DeviceNotFound: raise ResourceNotFound - if self.spec.get('virtual', {}).get('enabled', False): + if self.spec.get("virtual", {}).get("enabled", False): delete_hwil_device(device) device.delete() @@ -105,21 +112,28 @@ def to_v1(self) -> v1Device: rosbag_mount_path = None ros_workspace = None - docker_enabled = self.spec.get('docker', False) and self.spec.docker.enabled + docker_enabled = self.spec.get("docker", False) and self.spec.docker.enabled if docker_enabled: rosbag_mount_path = self.spec.docker.rosbagMountPath - preinstalled_enabled = self.spec.get('preinstalled', False) and self.spec.preinstalled.enabled - if preinstalled_enabled and self.spec.preinstalled.get('catkinWorkspace'): + preinstalled_enabled = ( + self.spec.get("preinstalled", False) and self.spec.preinstalled.enabled + ) + if preinstalled_enabled and self.spec.preinstalled.get("catkinWorkspace"): ros_workspace = self.spec.preinstalled.catkinWorkspace - config_variables = self.spec.get('configVariables', {}) - labels = self.metadata.get('labels', {}) + config_variables = self.spec.get("configVariables", {}) + labels = self.metadata.get("labels", {}) return v1Device( - name=self.metadata.name, description=self.spec.get('description'), - runtime_docker=docker_enabled, runtime_preinstalled=preinstalled_enabled, - ros_distro=self.spec.rosDistro, python_version=python_version, - rosbag_mount_path=rosbag_mount_path, ros_workspace=ros_workspace, - config_variables=config_variables, labels=labels, + name=self.metadata.name, + description=self.spec.get("description"), + runtime_docker=docker_enabled, + runtime_preinstalled=preinstalled_enabled, + ros_distro=self.spec.rosDistro, + python_version=python_version, + rosbag_mount_path=rosbag_mount_path, + ros_workspace=ros_workspace, + config_variables=config_variables, + labels=labels, ) diff --git a/riocli/device/onboard.py b/riocli/device/onboard.py index a6e78e4d..b68d8b6f 100644 --- a/riocli/device/onboard.py +++ b/riocli/device/onboard.py @@ -17,8 +17,8 @@ from riocli.device.util import name_to_guid -@click.command('onboard') -@click.argument('device-name', type=str) +@click.command("onboard") +@click.argument("device-name", type=str) @name_to_guid def device_onboard(device_name: str, device_guid: str) -> None: """Generates the on-boarding script for a device. @@ -33,5 +33,5 @@ def device_onboard(device_name: str, device_guid: str) -> None: script = device.onboard_script() click.secho(script.full_command()) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) diff --git a/riocli/device/tools/__init__.py b/riocli/device/tools/__init__.py index cd3e2200..9bff349d 100644 --- a/riocli/device/tools/__init__.py +++ b/riocli/device/tools/__init__.py @@ -25,8 +25,8 @@ @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color="yellow", + help_options_color="green", ) def tools(): """Tools for managing devices. diff --git a/riocli/device/tools/device_init.py b/riocli/device/tools/device_init.py index 348fe01e..ce038ee0 100644 --- a/riocli/device/tools/device_init.py +++ b/riocli/device/tools/device_init.py @@ -23,8 +23,8 @@ from riocli.utils.spinner import with_spinner -@click.command('init') -@click.argument('device-name', type=str) +@click.command("init") +@click.argument("device-name", type=str) @with_spinner(text="Initializing device...", timer=True) @name_to_guid def device_init(device_name: str, device_guid: str, spinner=None) -> None: @@ -39,33 +39,52 @@ def device_init(device_name: str, device_guid: str, spinner=None) -> None: _setup_local(spinner=spinner) spinner.text = click.style( - "Initialized device {}".format(device_name), fg=Colors.GREEN) + "Initialized device {}".format(device_name), fg=Colors.GREEN + ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: spinner.text = click.style( - "Failed to initialize device. Error: {}".format(e), fg=Colors.RED) + "Failed to initialize device. Error: {}".format(e), fg=Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) def _setup_device(device_guid: str, spinner=None) -> None: spinner.write("> Installing pre-requisites on device") - run_on_device(device_guid=device_guid, command=[ - 'apt', 'install', '-y', 'socat', - # TODO: Install piping-tunnel during onboarding itself - '&&', 'curl', '-sLO', - 'https://github.com/nwtgck/go-piping-tunnel/releases/download/v0.10.1/piping-tunnel-0.10.1-linux-amd64.deb', - '&&', 'dpkg', '-i', 'piping-tunnel-0.10.1-linux-amd64.deb', - '&&', 'rm', 'piping-tunnel-0.10.1-linux-amd64.deb', - '&&', 'mkdir', '-p', '/root/.ssh', - '&&', '/root/.ssh/authorized_keys' - ]) + run_on_device( + device_guid=device_guid, + command=[ + "apt", + "install", + "-y", + "socat", + # TODO: Install piping-tunnel during onboarding itself + "&&", + "curl", + "-sLO", + "https://github.com/nwtgck/go-piping-tunnel/releases/download/v0.10.1/piping-tunnel-0.10.1-linux-amd64.deb", + "&&", + "dpkg", + "-i", + "piping-tunnel-0.10.1-linux-amd64.deb", + "&&", + "rm", + "piping-tunnel-0.10.1-linux-amd64.deb", + "&&", + "mkdir", + "-p", + "/root/.ssh", + "&&", + "/root/.ssh/authorized_keys", + ], + ) def _setup_local(spinner=None) -> None: config = Configuration() - path = os.path.join(os.path.dirname(config.filepath), 'tools') - tunnel = os.path.join(path, 'piping-tunnel') + path = os.path.join(os.path.dirname(config.filepath), "tools") + tunnel = os.path.join(path, "piping-tunnel") if os.path.isfile(tunnel): spinner.write("> Tools already installed on local machine") return @@ -73,5 +92,7 @@ def _setup_local(spinner=None) -> None: # TODO: Add support for non-linux and non-amd64 machines spinner.write("> Installing pre-requisites locally...") run_bash("""/bin/bash -c 'mkdir -p {}'""".format(path)) - run_bash("""/bin/bash -c 'pushd {} && curl -sSLO https://github.com/nwtgck/go-piping-tunnel/releases/download/v0.10.1/piping-tunnel-0.10.1-linux-amd64.tar.gz && tar xf piping-tunnel-0.10.1-linux-amd64.tar.gz && rm CHANGELOG.md LICENSE piping-tunnel-0.10.1-linux-amd64.tar.gz README.md && popd' - """.format(path)) + run_bash( + """/bin/bash -c 'pushd {} && curl -sSLO https://github.com/nwtgck/go-piping-tunnel/releases/download/v0.10.1/piping-tunnel-0.10.1-linux-amd64.tar.gz && tar xf piping-tunnel-0.10.1-linux-amd64.tar.gz && rm CHANGELOG.md LICENSE piping-tunnel-0.10.1-linux-amd64.tar.gz README.md && popd' + """.format(path) + ) diff --git a/riocli/device/tools/forward.py b/riocli/device/tools/forward.py index bfb98f9b..9a945e15 100644 --- a/riocli/device/tools/forward.py +++ b/riocli/device/tools/forward.py @@ -22,22 +22,24 @@ @click.command( - 'port-forward', + "port-forward", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('device-name', type=str) -@click.argument('remote-port', type=int) -@click.argument('local-port', type=int, default=0, required=False) +@click.argument("device-name", type=str) +@click.argument("remote-port", type=int) +@click.argument("local-port", type=int, default=0, required=False) @name_to_guid -def port_forward(device_name: str, device_guid: str, remote_port: int, local_port: int) -> None: +def port_forward( + device_name: str, device_guid: str, remote_port: int, local_port: int +) -> None: """Forwards a port on the device to local machine.""" try: path = random_string(8, 5) if local_port == 0: local_port = get_free_tcp_port() - click.secho('Listening on local port {}'.format(local_port)) + click.secho("Listening on local port {}".format(local_port)) run_tunnel_on_device(device_guid=device_guid, remote_port=remote_port, path=path) run_tunnel_on_local(local_port=local_port, path=path, background=False) diff --git a/riocli/device/tools/rapyuta_logs.py b/riocli/device/tools/rapyuta_logs.py index 59f1207b..ff43b2fa 100644 --- a/riocli/device/tools/rapyuta_logs.py +++ b/riocli/device/tools/rapyuta_logs.py @@ -21,8 +21,8 @@ from riocli.utils.ssh_tunnel import get_free_tcp_port -@click.command('agent-logs', hidden=True) -@click.argument('device-name', type=str) +@click.command("agent-logs", hidden=True) +@click.argument("device-name", type=str) @name_to_guid def rapyuta_agent_logs(device_name: str, device_guid: str) -> None: """Stream rapyuta agent logs from the device. @@ -36,7 +36,10 @@ def rapyuta_agent_logs(device_name: str, device_guid: str) -> None: run_tunnel_on_device(device_guid=device_guid, remote_port=22, path=path) run_tunnel_on_local(local_port=local_port, path=path, background=True) os.system( - 'ssh -p {} -o StrictHostKeyChecking=no root@localhost tail -f /var/log/salt/minion'.format(local_port)) + "ssh -p {} -o StrictHostKeyChecking=no root@localhost tail -f /var/log/salt/minion".format( + local_port + ) + ) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) diff --git a/riocli/device/tools/scp.py b/riocli/device/tools/scp.py index 5baf0205..499145dc 100644 --- a/riocli/device/tools/scp.py +++ b/riocli/device/tools/scp.py @@ -23,14 +23,14 @@ @click.command( - 'scp', + "scp", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('source', nargs=1) -@click.argument('destination', nargs=1) -@with_spinner(text='Copying files...') +@click.argument("source", nargs=1) +@click.argument("destination", nargs=1) +@with_spinner(text="Copying files...") def scp(source: str, destination: str, spinner: Yaspin = None) -> None: """SCP like interface to copy files to and from a device. @@ -51,8 +51,10 @@ def scp(source: str, destination: str, spinner: Yaspin = None) -> None: dest_device_guid, dest = is_remote_path(destination, devices=devices) if src_device_guid is None and dest_device_guid is None: - raise Exception('One of source or destination paths should be a remote ' - 'path of the format :path') + raise Exception( + "One of source or destination paths should be a remote " + "path of the format :path" + ) if src_device_guid is not None: with spinner.hidden(): @@ -62,9 +64,9 @@ def scp(source: str, destination: str, spinner: Yaspin = None) -> None: with spinner.hidden(): copy_to_device(dest_device_guid, src, dest) - spinner.text = click.style('Files copied successfully', fg=Colors.GREEN) + spinner.text = click.style("Files copied successfully", fg=Colors.GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style('Failed to copy files: {}'.format(e), fg=Colors.RED) + spinner.text = click.style("Failed to copy files: {}".format(e), fg=Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/device/tools/service.py b/riocli/device/tools/service.py index 1bb22801..6d8b1247 100644 --- a/riocli/device/tools/service.py +++ b/riocli/device/tools/service.py @@ -17,88 +17,86 @@ from riocli.utils.execute import run_on_device -@click.command('status-all') -@click.argument('device-name', type=str) +@click.command("status-all") +@click.argument("device-name", type=str) @name_to_guid def status_all(device_name: str, device_guid: str) -> None: """Get the status of all services on the device.""" try: - remote_service_cmd_list = ['service', '--status-all'] + remote_service_cmd_list = ["service", "--status-all"] response = run_on_device(device_guid=device_guid, command=remote_service_cmd_list) click.secho(response) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) def run_service_cmd(device_guid, service_name, service_cmd=""): """Run a service command on the device.""" try: - remote_service_cmd_list = ['service', service_name, service_cmd] + remote_service_cmd_list = ["service", service_name, service_cmd] response = run_on_device(device_guid=device_guid, command=remote_service_cmd_list) click.secho(response) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) -@click.command('status') -@click.argument('device-name', type=str) -@click.argument('service-name', nargs=1) +@click.command("status") +@click.argument("device-name", type=str) +@click.argument("service-name", nargs=1) @name_to_guid def status(device_name: str, device_guid: str, service_name) -> None: """Get the status of a service on the device.""" - run_service_cmd(device_guid, service_name, 'status') + run_service_cmd(device_guid, service_name, "status") -@click.command('start') -@click.argument('device-name', type=str) -@click.argument('service-name', nargs=1) +@click.command("start") +@click.argument("device-name", type=str) +@click.argument("service-name", nargs=1) @name_to_guid def start(device_name: str, device_guid: str, service_name) -> None: """Start a service on the device.""" - run_service_cmd(device_guid, service_name, 'start') + run_service_cmd(device_guid, service_name, "start") -@click.command('stop') -@click.argument('device-name', type=str) -@click.argument('service-name', nargs=1) +@click.command("stop") +@click.argument("device-name", type=str) +@click.argument("service-name", nargs=1) @name_to_guid def stop(device_name: str, device_guid: str, service_name) -> None: """Stop a service on the device.""" - run_service_cmd(device_guid, service_name, 'stop') + run_service_cmd(device_guid, service_name, "stop") -@click.command('reload') -@click.argument('device-name', type=str) -@click.argument('service-name', nargs=1) +@click.command("reload") +@click.argument("device-name", type=str) +@click.argument("service-name", nargs=1) @name_to_guid def reload(device_name: str, device_guid: str, service_name) -> None: """Reload a service on the device.""" - run_service_cmd(device_guid, service_name, 'reload') + run_service_cmd(device_guid, service_name, "reload") -@click.command('force-reload') -@click.argument('device-name', type=str) -@click.argument('service-name', nargs=1) +@click.command("force-reload") +@click.argument("device-name", type=str) +@click.argument("service-name", nargs=1) @name_to_guid def force_reload(device_name: str, device_guid: str, service_name) -> None: """Force reload a service on the device.""" - run_service_cmd(device_guid, service_name, 'force_reload') + run_service_cmd(device_guid, service_name, "force_reload") -@click.command('restart') -@click.argument('device-name', type=str) -@click.argument('service-name', nargs=1) +@click.command("restart") +@click.argument("device-name", type=str) +@click.argument("service-name", nargs=1) @name_to_guid def restart(device_name: str, device_guid: str, service_name) -> None: """Restart a service on the device.""" - run_service_cmd(device_guid, service_name, 'restart') + run_service_cmd(device_guid, service_name, "restart") -@click.group( - hidden=True -) +@click.group(hidden=True) def service(): """System management commands for services on the device. diff --git a/riocli/device/tools/ssh.py b/riocli/device/tools/ssh.py index dd78e1d8..f0f051ca 100644 --- a/riocli/device/tools/ssh.py +++ b/riocli/device/tools/ssh.py @@ -17,7 +17,11 @@ from click_help_colors import HelpColorsCommand from riocli.constants import Colors, Symbols -from riocli.device.tools.util import (copy_to_device, run_tunnel_on_device, run_tunnel_on_local) +from riocli.device.tools.util import ( + copy_to_device, + run_tunnel_on_device, + run_tunnel_on_local, +) from riocli.device.util import name_to_guid from riocli.utils import random_string from riocli.utils.execute import run_on_device @@ -26,27 +30,40 @@ @click.command( - 'ssh', + "ssh", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--user', '-u', default='root', help='Username for the SSH') -@click.option('--local-port', '-L', default=None, - help='Port number on the local machine for forwarding SSH') -@click.option('--remote-port', '-R', default=22, - help='Port number on the Device on which SSH Server is listening') -@click.option('--x-forward/--no-x-forward', '-X', default=False, is_flag=True, - help='Flag to enable X Forwarding over SSH') -@click.argument('device-name', type=str) +@click.option("--user", "-u", default="root", help="Username for the SSH") +@click.option( + "--local-port", + "-L", + default=None, + help="Port number on the local machine for forwarding SSH", +) +@click.option( + "--remote-port", + "-R", + default=22, + help="Port number on the Device on which SSH Server is listening", +) +@click.option( + "--x-forward/--no-x-forward", + "-X", + default=False, + is_flag=True, + help="Flag to enable X Forwarding over SSH", +) +@click.argument("device-name", type=str) @name_to_guid def device_ssh( - device_name: str, - device_guid: str, - user: str, - local_port: int, - remote_port: int, - x_forward: bool, + device_name: str, + device_guid: str, + user: str, + local_port: int, + remote_port: int, + x_forward: bool, ) -> None: """SSH into a device. @@ -73,32 +90,38 @@ def device_ssh( path = random_string(8, 5) if not local_port: local_port = get_free_tcp_port() - run_tunnel_on_device(device_guid=device_guid, remote_port=remote_port, - path=path) + run_tunnel_on_device(device_guid=device_guid, remote_port=remote_port, path=path) run_tunnel_on_local(local_port=local_port, path=path, background=True) os.system( - 'ssh -p {} {} -o StrictHostKeyChecking=no {}@localhost'.format( - local_port, extra_args, user)) + "ssh -p {} {} -o StrictHostKeyChecking=no {}@localhost".format( + local_port, extra_args, user + ) + ) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) @click.command( - 'ssh-authorize', + "ssh-authorize", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--user', '-u', default='root', - help='User for which SSH keys are added') -@click.argument('device-name', type=str) -@click.argument('public-key-file', default="~/.ssh/id_rsa.pub", - type=click.Path(exists=True)) +@click.option("--user", "-u", default="root", help="User for which SSH keys are added") +@click.argument("device-name", type=str) +@click.argument( + "public-key-file", default="~/.ssh/id_rsa.pub", type=click.Path(exists=True) +) @with_spinner(text="Authorizing public SSH key...", timer=True) @name_to_guid -def ssh_authorize_key(device_name: str, device_guid: str, user: str, - public_key_file: click.Path, spinner=None) -> None: +def ssh_authorize_key( + device_name: str, + device_guid: str, + user: str, + public_key_file: click.Path, + spinner=None, +) -> None: """Authorize your local machine's public SSH key This command will add the public SSH key of your local machine to the @@ -124,16 +147,25 @@ def ssh_authorize_key(device_name: str, device_guid: str, user: str, with spinner.hidden(): copy_to_device(device_guid, str(public_key_file), temp_path) - if user != 'root': - command = ['cat', temp_path, '>>', '/home/' + user + '/.ssh/authorized_keys'] + if user != "root": + command = [ + "cat", + temp_path, + ">>", + "/home/" + user + "/.ssh/authorized_keys", + ] else: - command = ['cat', temp_path, '>>', '/root/.ssh/authorized_keys'] + command = ["cat", temp_path, ">>", "/root/.ssh/authorized_keys"] run_on_device(device_guid=device_guid, command=command, user=user) - spinner.text = click.style('Public key {} added successfully'.format(public_key_file), fg=Colors.GREEN) + spinner.text = click.style( + "Public key {} added successfully".format(public_key_file), fg=Colors.GREEN + ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style("Failed to add keys".format(e), fg=Colors.RED) + spinner.text = click.style( + "Failed to add keys. Error: {}".format(e), fg=Colors.RED + ) spinner.red.ok(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/device/tools/util.py b/riocli/device/tools/util.py index 567db8b1..4a8d9433 100644 --- a/riocli/device/tools/util.py +++ b/riocli/device/tools/util.py @@ -24,24 +24,35 @@ def run_tunnel_on_device(device_guid: str, remote_port: int, path: str) -> None: config = Configuration() - run_on_device(device_guid=device_guid, - command=['piping-tunnel', 'server', '--server', config.piping_server, '--port', - str(remote_port), path], - background=True) + run_on_device( + device_guid=device_guid, + command=[ + "piping-tunnel", + "server", + "--server", + config.piping_server, + "--port", + str(remote_port), + path, + ], + background=True, + ) def run_tunnel_on_local(local_port: int, path: str, background: bool = False) -> None: config = Configuration() - tunnel = os.path.join(os.path.dirname(config.filepath), 'tools', 'piping-tunnel') - command = '{} client --server {} --port {} {}'.format(tunnel, config.piping_server, local_port, path) + tunnel = os.path.join(os.path.dirname(config.filepath), "tools", "piping-tunnel") + command = "{} client --server {} --port {} {}".format( + tunnel, config.piping_server, local_port, path + ) if background: - command = '{} --progress=false'.format(command) + command = "{} --progress=false".format(command) click.secho(command) run_bash(command, bg=background) def copy_from_device(device_guid: str, src: str, dest: str) -> None: - file = '{}-{}'.format(src, random_string(7, 5)).lstrip('/').replace('/', '-') + file = "{}-{}".format(src, random_string(7, 5)).lstrip("/").replace("/", "-") client = new_client() device = client.get_device(device_id=device_guid) request_uuid = device.upload_log_file(LogsUploadRequest(src, file_name=file)) @@ -54,7 +65,9 @@ def copy_from_device(device_guid: str, src: str, dest: str) -> None: continue if status.status != "COMPLETED": - raise Exception('Upload status: {}'.format(status.status, status.error_message)) + raise Exception( + "Upload status: {} Error: {}".format(status.status, status.error_message) + ) url = device.download_log_file(request_uuid) run_bash('curl -o "{}" "{}"'.format(dest, url)) @@ -63,7 +76,8 @@ def copy_from_device(device_guid: str, src: str, dest: str) -> None: def copy_to_device(device_guid: str, src: str, dest: str) -> None: config = Configuration() path = random_string(8, 5) - run_bash('curl -sT {} {}/{}'.format(src, config.piping_server, path), bg=True) + run_bash("curl -sT {} {}/{}".format(src, config.piping_server, path), bg=True) run_on_device( device_guid=device_guid, - command=['curl', '-s', '-o', dest, '{}/{}'.format(config.piping_server, path)]) + command=["curl", "-s", "-o", dest, "{}/{}".format(config.piping_server, path)], + ) diff --git a/riocli/device/topic.py b/riocli/device/topic.py index 3dd3f804..e71e4f8f 100644 --- a/riocli/device/topic.py +++ b/riocli/device/topic.py @@ -23,11 +23,11 @@ @click.group( - 'topics', + "topics", invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color="yellow", + help_options_color="green", ) def device_topics(): """ @@ -36,8 +36,8 @@ def device_topics(): pass -@device_topics.command('list') -@click.argument('device-name', type=str) +@device_topics.command("list") +@click.argument("device-name", type=str) @name_to_guid def list_topics(device_name: str, device_guid: str) -> None: """ @@ -48,14 +48,14 @@ def list_topics(device_name: str, device_guid: str) -> None: device = client.get_device(device_id=device_guid) _display_topic_list(device.topic_status()) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) -@device_topics.command('subscribe') -@click.argument('device-name', type=str) -@click.argument('topic', type=str) -@click.argument('kind', type=click.Choice(['metric', 'log'])) +@device_topics.command("subscribe") +@click.argument("device-name", type=str) +@click.argument("topic", type=str) +@click.argument("kind", type=click.Choice(["metric", "log"])) @name_to_guid def subscribe_topic(device_name: str, device_guid: str, topic: str, kind: str) -> None: """ @@ -67,16 +67,16 @@ def subscribe_topic(device_name: str, device_guid: str, topic: str, kind: str) - device = client.get_device(device_id=device_guid) kind = TopicKind(kind.upper()) device.subscribe_topic(topic, qos=QoS.LOW.value, kind=kind) - click.secho('Topic subscribed successfully', fg='green') + click.secho("Topic subscribed successfully", fg="green") except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) -@device_topics.command('unsubscribe') -@click.argument('device-name', type=str) -@click.argument('topic', type=str) -@click.argument('kind', type=click.Choice(['metric', 'log'])) +@device_topics.command("unsubscribe") +@click.argument("device-name", type=str) +@click.argument("topic", type=str) +@click.argument("kind", type=click.Choice(["metric", "log"])) @name_to_guid def unsubscribe_topic(device_name: str, device_guid: str, topic: str, kind: str) -> None: """ @@ -88,30 +88,30 @@ def unsubscribe_topic(device_name: str, device_guid: str, topic: str, kind: str) device = client.get_device(device_id=device_guid) kind = TopicKind(kind.upper()) device.unsubscribe_topic(topic, kind=kind) - click.secho('Topic un-subscribed successfully', fg='green') + click.secho("Topic un-subscribed successfully", fg="green") except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) def _display_topic_list(status: TopicsStatus, show_header: bool = True) -> None: if status.master_up: - click.secho('ROS Master is {}'.format(click.style('Up', fg='green'))) + click.secho("ROS Master is {}".format(click.style("Up", fg="green"))) else: - click.secho('ROS Master is {}'.format(click.style('Down', fg='red'))) + click.secho("ROS Master is {}".format(click.style("Down", fg="red"))) headers = [] if show_header: - headers = ('Name', 'Type', 'Status') + headers = ("Name", "Type", "Status") data = [] for topic in status.Subscribed.metric: - data.append([topic.name, 'Metric', 'Subscribed']) + data.append([topic.name, "Metric", "Subscribed"]) for topic in status.Subscribed.log: - data.append([topic.name, 'Log', 'Subscribed']) + data.append([topic.name, "Log", "Subscribed"]) for topic in status.Unsubscribed: - data.append([topic, '', 'Un-Subscribed']) + data.append([topic, "", "Un-Subscribed"]) tabulate_data(data, headers) diff --git a/riocli/device/util.py b/riocli/device/util.py index 773194b4..0dbc6d24 100644 --- a/riocli/device/util.py +++ b/riocli/device/util.py @@ -44,7 +44,7 @@ def decorated(**kwargs: typing.Any): click.secho(str(e), fg=Colors.RED) raise SystemExit(1) - name = kwargs.pop('device_name') + name = kwargs.pop("device_name") # device_name is not specified if name is None: @@ -67,8 +67,8 @@ def decorated(**kwargs: typing.Any): click.secho(str(e), fg=Colors.RED) raise SystemExit(1) - kwargs['device_name'] = name - kwargs['device_guid'] = guid + kwargs["device_name"] = name + kwargs["device_guid"] = guid f(**kwargs) return decorated @@ -105,52 +105,61 @@ def decorated(**kwargs): click.secho(str(e), fg=Colors.RED) raise SystemExit(1) - file_name = kwargs.pop('file_name') + file_name = kwargs.pop("file_name") - device_guid = kwargs.get('device_guid') + device_guid = kwargs.get("device_guid") device = client.get_device(device_id=device_guid) requests = device.list_uploaded_files_for_device(filter_by_filename=file_name) file_name, request_id = find_request_id(requests, file_name) - kwargs['file_name'] = file_name - kwargs['request_id'] = request_id + kwargs["file_name"] = file_name + kwargs["request_id"] = request_id f(**kwargs) return decorated def fetch_devices( - client: Client, - device_name_or_regex: str, - include_all: bool, - online_devices: bool = False + client: Client, + device_name_or_regex: str, + include_all: bool, + online_devices: bool = False, ) -> typing.List[Device]: devices = client.get_all_devices(online_device=online_devices) result = [] for device in devices: - if (include_all or device.name == device_name_or_regex or - device_name_or_regex == device.uuid or - (device_name_or_regex not in device.name and - re.search(r'^{}$'.format(device_name_or_regex), device.name))): + if ( + include_all + or device.name == device_name_or_regex + or device_name_or_regex == device.uuid + or ( + device_name_or_regex not in device.name + and re.search(r"^{}$".format(device_name_or_regex), device.name) + ) + ): result.append(device) return result -def migrate_device_to_project(ctx: click.Context, device_id: str, dest_project_id: str) -> None: +def migrate_device_to_project( + ctx: click.Context, device_id: str, dest_project_id: str +) -> None: config = get_config_from_context(ctx) - host = config.data.get('core_api_host', 'https://gaapiserver.apps.okd4v2.prod.rapyuta.io') - url = '{}/api/device-manager/v0/devices/{}/migrate'.format(host, device_id) + host = config.data.get( + "core_api_host", "https://gaapiserver.apps.okd4v2.prod.rapyuta.io" + ) + url = "{}/api/device-manager/v0/devices/{}/migrate".format(host, device_id) headers = config.get_auth_header() - payload = {'project': dest_project_id} + payload = {"project": dest_project_id} response = RestClient(url).method(HttpMethod.PUT).headers(headers).execute(payload) - err_msg = 'error in the api call' + err_msg = "error in the api call" data = json.loads(response.text) if not response.ok: - err_msg = data.get('response', {}).get('error', '') + err_msg = data.get("response", {}).get("error", "") raise Exception(err_msg) @@ -163,17 +172,9 @@ def find_request_id(requests: typing.List[LogUploads], file_name: str) -> (str, raise SystemExit(1) -def device_identity(src, devices=[]): - if is_valid_uuid(src): - return src - else: - for device in devices: - if device.name == src: - return device.uuid - return None +def is_remote_path(src, devices=None): + devices = devices or [] - -def is_remote_path(src, devices=[]): if ":" in src: parts = src.split(":") if len(parts) == 2: @@ -188,12 +189,12 @@ def is_remote_path(src, devices=[]): def create_hwil_device(spec: dict, metadata: dict) -> Munch: """Create a new hardware-in-the-loop device.""" - virtual = spec['virtual'] - os = virtual['os'] - codename = virtual['codename'] - arch = virtual['arch'] - product = virtual['product'] - name = metadata['name'] + virtual = spec["virtual"] + os = virtual["os"] + codename = virtual["codename"] + arch = virtual["arch"] + product = virtual["product"] + name = metadata["name"] labels = make_hwil_labels(virtual, name) device_name = sanitize_hwil_device_name(f"{name}-{product}-{labels['user']}") @@ -209,8 +210,8 @@ def create_hwil_device(spec: dict, metadata: dict) -> Munch: response = client.create_device(device_name, arch, os, codename, labels) client.poll_till_device_ready(response.id, sleep_interval=5, retry_limit=12) - if response.status == 'FAILED': - raise Exception('device has failed') + if response.status == "FAILED": + raise Exception("device has failed") return response @@ -221,19 +222,19 @@ def delete_hwil_device(device: Device) -> None: This is a helper method that deletes a HWIL device associated with the rapyuta.io device. """ - labels = device.get('labels', {}) + labels = device.get("labels", {}) if not labels: - raise DeviceNotFound(message='hwil device not found') + raise DeviceNotFound(message="hwil device not found") device_id = None - for l in labels: - if l['key'] == 'hwil_device_id': - device_id = l['value'] + for label in labels: + if label["key"] == "hwil_device_id": + device_id = label["value"] break if device_id is None: - raise DeviceNotFound(message='hwil device not found') + raise DeviceNotFound(message="hwil device not found") client = new_hwil_client() client.delete_device(device_id) @@ -252,14 +253,14 @@ def execute_onboard_command(device_id: int, onboard_command: str) -> None: def make_hwil_labels(spec: dict, device_name: str) -> typing.Dict: data = Configuration().data - user_email = data['email_id'] - user_email = user_email.split('@')[0] + user_email = data["email_id"] + user_email = user_email.split("@")[0] labels = { "user": user_email, - "organization": data['organization_id'], - "project": data['project_id'], - "product": spec['product'], + "organization": data["organization_id"], + "project": data["project_id"], + "product": spec["product"], "rapyuta_device_name": device_name, } @@ -287,9 +288,9 @@ def sanitize_hwil_device_name(name): name = trim_suffix(name) name = trim_prefix(name) - r = '' + r = "" for c in name: - if c.isalnum() or c in ['-', '_']: + if c.isalnum() or c in ["-", "_"]: r = r + c return r @@ -306,10 +307,14 @@ def wait_until_online(device: Device, timeout: int = 600) -> None: device.refresh() - while not device.is_online() and device.status not in failed_states and counter < timeout: + while ( + not device.is_online() + and device.status not in failed_states + and counter < timeout + ): counter += interval time.sleep(interval) device.refresh() if not device.is_online() and counter >= timeout: - raise Exception('timeout reached while waiting for the device to be online') + raise Exception("timeout reached while waiting for the device to be online") diff --git a/riocli/device/vpn.py b/riocli/device/vpn.py index 5066f037..83fe29fc 100644 --- a/riocli/device/vpn.py +++ b/riocli/device/vpn.py @@ -28,25 +28,41 @@ @click.command( - 'vpn', + "vpn", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, - help_options_color=Colors.GREEN + help_options_color=Colors.GREEN, +) +@click.option( + "--devices", + type=click.STRING, + multiple=True, + default=(), + help="Device names to toggle VPN client", +) +@click.argument("enable", type=click.BOOL) +@click.option( + "-f", + "--force", + "--silent", + "silent", + is_flag=True, + type=click.BOOL, + default=False, + help="Skip confirmation", +) +@click.option( + "--advertise-routes", + is_flag=True, + type=click.BOOL, + default=False, + help="Advertise subnets configured in project to VPN peers", ) -@click.option('--devices', type=click.STRING, multiple=True, default=(), - help='Device names to toggle VPN client') -@click.argument('enable', type=click.BOOL) -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, - type=click.BOOL, default=False, - help="Skip confirmation") -@click.option('--advertise-routes', is_flag=True, - type=click.BOOL, default=False, - help="Advertise subnets configured in project to VPN peers") def toggle_vpn( - devices: typing.List, - enable: bool, - silent: bool = False, - advertise_routes: bool = False, + devices: typing.List, + enable: bool, + silent: bool = False, + advertise_routes: bool = False, ) -> None: """Enable or disable VPN client on the device. @@ -89,45 +105,54 @@ def toggle_vpn( click.secho( "\nSetting the state of VPN client = {} on " - "the following online devices\n".format(enable), fg='yellow') + "the following online devices\n".format(enable), + fg="yellow", + ) print_final_devices(final) if not silent: if advertise_routes and len(final) > 1: - msg = ("\n{} More than one device in the project will advertise routes. " - "You may not want to do that. Please review the devices.".format(Symbols.WARNING)) - click.secho(msg, fg='yellow') + msg = ( + "\n{} More than one device in the project will advertise routes. " + "You may not want to do that. Please review the devices.".format( + Symbols.WARNING + ) + ) + click.secho(msg, fg="yellow") - click.confirm( - "\nDo you want to proceed?", - default=True, abort=True) + click.confirm("\nDo you want to proceed?", default=True, abort=True) click.echo("") # Echo an empty line result = [] with Spinner() as spinner: for device in final: - spinner.text = 'Updating VPN state on device {}'.format( - click.style(device.name, bold=True, fg=Colors.CYAN)) + spinner.text = "Updating VPN state on device {}".format( + click.style(device.name, bold=True, fg=Colors.CYAN) + ) r = client.toggle_features( - device.uuid, [('vpn', enable)], - config={'vpn': {'advertise_routes': advertise_routes}} + device.uuid, + [("vpn", enable)], + config={"vpn": {"advertise_routes": advertise_routes}}, ) - result.append([device.name, r.get('status')]) + result.append([device.name, r.get("status")]) click.echo("") # Echo an empty line tabulate_data(result, headers=["Device", "Status"]) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) from e def process_devices(online_devices, devices) -> typing.List: if len(devices) == 0: - click.secho("\n(No devices specified. State will be applied" - " to all online devices in the project)", fg='cyan') + click.secho( + "\n(No devices specified. State will be applied" + " to all online devices in the project)", + fg="cyan", + ) return online_devices name_map, uuid_map = {}, {} diff --git a/riocli/disk/create.py b/riocli/disk/create.py index 1c0cfb11..00540e05 100644 --- a/riocli/disk/create.py +++ b/riocli/disk/create.py @@ -38,22 +38,29 @@ @click.command( - 'create', + "create", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('disk-name', type=str) -@click.option('--capacity', 'capacity', type=click.INT, - default=DiskCapacity.GiB_4.value, help='Disk size in GiB. [4|8|16|32|64|128|256|512]') -@click.option('--region', 'region', type=click.Choice(SUPPORTED_REGIONS), - default=Regions.JP, help='Region to create the disk in') +@click.argument("disk-name", type=str) +@click.option( + "--capacity", + "capacity", + type=click.INT, + default=DiskCapacity.GiB_4.value, + help="Disk size in GiB. [4|8|16|32|64|128|256|512]", +) +@click.option( + "--region", + "region", + type=click.Choice(SUPPORTED_REGIONS), + default=Regions.JP, + help="Region to create the disk in", +) @with_spinner(text="Creating a new disk...") def create_disk( - disk_name: str, - capacity: int = 4, - region: str = 'jp', - spinner: Yaspin = None + disk_name: str, capacity: int = 4, region: str = "jp", spinner: Yaspin = None ) -> None: """Creates a new cloud disk. @@ -69,24 +76,18 @@ def create_disk( """ client = new_v2_client() payload = { - "metadata": { - "name": disk_name, - "region": region - }, - "spec": { - "capacity": capacity, - "runtime": "cloud" - } + "metadata": {"name": disk_name, "region": region}, + "spec": {"capacity": capacity, "runtime": "cloud"}, } try: client.create_disk(payload) client.poll_disk(disk_name) spinner.text = click.style( - 'Disk {} created successfully.'. - format(disk_name), fg=Colors.GREEN) + "Disk {} created successfully.".format(disk_name), fg=Colors.GREEN + ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style('Failed to create disk: {}'.format(e), fg=Colors.RED) + spinner.text = click.style("Failed to create disk: {}".format(e), fg=Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/disk/delete.py b/riocli/disk/delete.py index 404fc2ae..fcffdff0 100644 --- a/riocli/disk/delete.py +++ b/riocli/disk/delete.py @@ -29,26 +29,37 @@ @click.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--force', '-f', is_flag=True, default=False, - help='Skip confirmation', type=bool) -@click.option('-a', '--all', 'delete_all', is_flag=True, default=False, - help='Deletes all disks in the project') -@click.option('--workers', '-w', - help="Number of parallel workers while running deleting disks. Defaults to 10", - type=int, default=10) -@click.argument('disk-name-or-regex', type=str) -@with_spinner(text='Deleting disk...') +@click.option( + "--force", "-f", is_flag=True, default=False, help="Skip confirmation", type=bool +) +@click.option( + "-a", + "--all", + "delete_all", + is_flag=True, + default=False, + help="Deletes all disks in the project", +) +@click.option( + "--workers", + "-w", + help="Number of parallel workers while running deleting disks. Defaults to 10", + type=int, + default=10, +) +@click.argument("disk-name-or-regex", type=str) +@with_spinner(text="Deleting disk...") def delete_disk( - force: bool, - disk_name_or_regex: str, - delete_all: bool = False, - workers: int = 10, - spinner: Yaspin = None + force: bool, + disk_name_or_regex: str, + delete_all: bool = False, + workers: int = 10, + spinner: Yaspin = None, ) -> None: """Delete one or more disks with a name or a regex pattern. @@ -89,8 +100,7 @@ def delete_disk( try: disks = fetch_disks(client, disk_name_or_regex, delete_all) except Exception as e: - spinner.text = click.style( - 'Failed to find disk(s): {}'.format(e), Colors.RED) + spinner.text = click.style("Failed to find disk(s): {}".format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -102,17 +112,18 @@ def delete_disk( with spinner.hidden(): display_disk_list(disks) - spinner.write('') + spinner.write("") if not force: with spinner.hidden(): - click.confirm('Do you want to delete the above disk(s)?', default=True, abort=True) + click.confirm( + "Do you want to delete the above disk(s)?", default=True, abort=True + ) try: f = functools.partial(_apply_delete, client) result = apply_func_with_result( - f=f, items=disks, - workers=workers, key=lambda x: x[0] + f=f, items=disks, workers=workers, key=lambda x: x[0] ) data, statuses = [], [] for name, status, msg in result: @@ -120,18 +131,17 @@ def delete_disk( icon = Symbols.SUCCESS if status else Symbols.ERROR statuses.append(status) - data.append([ - click.style(name, fg), - click.style('{} {}'.format(icon, msg), fg) - ]) + data.append( + [click.style(name, fg), click.style("{} {}".format(icon, msg), fg)] + ) with spinner.hidden(): - tabulate_data(data, headers=['Name', 'Status']) + tabulate_data(data, headers=["Name", "Status"]) # When no disk is deleted, raise an exception. if not any(statuses): - spinner.write('') - spinner.text = click.style('Failed to delete disk(s).', Colors.RED) + spinner.write("") + spinner.text = click.style("Failed to delete disk(s).", Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) @@ -139,12 +149,10 @@ def delete_disk( fg = Colors.GREEN if all(statuses) else Colors.YELLOW text = "successfully" if all(statuses) else "partially" - spinner.text = click.style( - 'Disk(s) deleted {}.'.format(text), fg) + spinner.text = click.style("Disk(s) deleted {}.".format(text), fg) spinner.ok(click.style(icon, fg)) except Exception as e: - spinner.text = click.style( - 'Failed to delete disk(s): {}'.format(e), Colors.RED) + spinner.text = click.style("Failed to delete disk(s): {}".format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -152,6 +160,6 @@ def delete_disk( def _apply_delete(client: Client, result: Queue, disk: Disk) -> None: try: client.delete_disk(name=disk.metadata.name) - result.put((disk.metadata.name, True, 'Disk deleted successfully')) + result.put((disk.metadata.name, True, "Disk deleted successfully")) except Exception as e: result.put((disk.metadata.name, False, str(e))) diff --git a/riocli/disk/enum.py b/riocli/disk/enum.py index 2629ad25..5ac4c1c7 100644 --- a/riocli/disk/enum.py +++ b/riocli/disk/enum.py @@ -24,4 +24,4 @@ def __str__(self): GiB_64 = 64 GiB_128 = 128 GiB_256 = 256 - GiB_512 = 512 \ No newline at end of file + GiB_512 = 512 diff --git a/riocli/disk/list.py b/riocli/disk/list.py index 3b84776f..37d70414 100644 --- a/riocli/disk/list.py +++ b/riocli/disk/list.py @@ -22,13 +22,20 @@ @click.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--label', '-l', 'labels', multiple=True, type=click.STRING, - default=(), help='Filter the disk list by labels') +@click.option( + "--label", + "-l", + "labels", + multiple=True, + type=click.STRING, + default=(), + help="Filter the disk list by labels", +) def list_disks(labels: typing.List[str]) -> None: """List the disks in the current project. @@ -41,7 +48,7 @@ def list_disks(labels: typing.List[str]) -> None: """ try: client = new_v2_client(with_project=True) - disks = client.list_disks(query={'labelSelector': labels}) + disks = client.list_disks(query={"labelSelector": labels}) display_disk_list(disks, show_header=True) except Exception as e: click.secho(str(e), fg=Colors.RED) diff --git a/riocli/disk/model.py b/riocli/disk/model.py index e4937a6b..ee0a381b 100644 --- a/riocli/disk/model.py +++ b/riocli/disk/model.py @@ -32,8 +32,8 @@ def apply(self, *args, **kwargs) -> ApplyResult: self.metadata.createdAt = None self.metadata.updatedAt = None - retry_count = int(kwargs.get('retry_count')) - retry_interval = int(kwargs.get('retry_interval')) + retry_count = int(kwargs.get("retry_count")) + retry_interval = int(kwargs.get("retry_interval")) try: r = client.create_disk(unmunchify(self)) diff --git a/riocli/disk/util.py b/riocli/disk/util.py index 824fbd41..df8411ba 100644 --- a/riocli/disk/util.py +++ b/riocli/disk/util.py @@ -11,8 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import typing import re +import typing from riocli.disk.model import Disk from riocli.utils import tabulate_data @@ -20,9 +20,9 @@ def fetch_disks( - client: Client, - disk_name_or_regex: str, - include_all: bool, + client: Client, + disk_name_or_regex: str, + include_all: bool, ) -> typing.List[Disk]: disks = client.list_disks() @@ -41,8 +41,12 @@ def display_disk_list(disks: typing.Any, show_header: bool = True): headers = [] if show_header: headers = ( - 'Disk ID', 'Name', 'Status', 'Capacity (GB)', - 'Capacity Used (GB)', 'Used By', + "Disk ID", + "Name", + "Status", + "Capacity (GB)", + "Capacity Used (GB)", + "Used By", ) data = [] @@ -50,11 +54,15 @@ def display_disk_list(disks: typing.Any, show_header: bool = True): for d in disks: capacity = d.status.get("capacityUsed", 0) / (1024 * 1204 * 1024) # Bytes -> GB - data.append([d.metadata.guid, - d.metadata.name, - d.status.get("status"), - d.spec.capacity, - capacity, - d.status.get("diskBound", {}).get("deployment_name")]) + data.append( + [ + d.metadata.guid, + d.metadata.name, + d.status.get("status"), + d.spec.capacity, + capacity, + d.status.get("diskBound", {}).get("deployment_name"), + ] + ) tabulate_data(data, headers) diff --git a/riocli/exceptions/__init__.py b/riocli/exceptions/__init__.py index b956b5bc..e55c4117 100644 --- a/riocli/exceptions/__init__.py +++ b/riocli/exceptions/__init__.py @@ -12,16 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -class NoProjectSelected(Exception): +class NoProjectSelected(Exception): def __str__(self): return """No project is selected. Select a project first - $ rio project select + $ rio project select """ class NoOrganizationSelected(Exception): - def __str__(self): return """No organization is selected. Select an organization first $ rio organization select @@ -29,28 +28,26 @@ def __str__(self): class LoggedOut(Exception): - def __str__(self): return """Not logged in. Please login first - $ rio auth login + $ rio auth login """ class HwilLoggedOut(Exception): - def __str__(self): return """Not logged in to HWIL. Please login first - $ rio hwil login + $ rio hwil login """ class DeviceNotFound(Exception): - def __init__(self, message='device not found'): + def __init__(self, message="device not found"): self.message = message super().__init__(self.message) class ResourceNotFound(Exception): - def __init__(self, message='resource not found'): + def __init__(self, message="resource not found"): self.message = message super().__init__(self.message) diff --git a/riocli/hwil/create.py b/riocli/hwil/create.py index 1e9d30a3..ab369862 100644 --- a/riocli/hwil/create.py +++ b/riocli/hwil/create.py @@ -23,27 +23,42 @@ @click.command( - 'create', + "create", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--arch', 'arch', help='Device architecture', - type=click.Choice(['amd64', 'arm64']), default='amd64') -@click.option('--os', 'os', help='Type of the OS', - type=click.Choice(['debian', 'ubuntu']), default='ubuntu') -@click.option('--codename', 'codename', help='Code name of the OS', - type=click.Choice(['bionic', 'focal', 'jammy', 'bullseye']), default='focal') -@click.argument('device-name', type=str) -@with_spinner(text='Creating device...') +@click.option( + "--arch", + "arch", + help="Device architecture", + type=click.Choice(["amd64", "arm64"]), + default="amd64", +) +@click.option( + "--os", + "os", + help="Type of the OS", + type=click.Choice(["debian", "ubuntu"]), + default="ubuntu", +) +@click.option( + "--codename", + "codename", + help="Code name of the OS", + type=click.Choice(["bionic", "focal", "jammy", "bullseye"]), + default="focal", +) +@click.argument("device-name", type=str) +@with_spinner(text="Creating device...") @click.pass_context def create_device( - ctx: click.Context, - device_name: str, - arch: str, - os: str, - codename: str, - spinner: Yaspin = None, + ctx: click.Context, + device_name: str, + arch: str, + os: str, + codename: str, + spinner: Yaspin = None, ) -> None: """Create a new hardware-in-the-loop device. @@ -74,29 +89,34 @@ def create_device( Note: All combinations of the --arch, --os and --codename flags may not always work. Please contact io-support for more information. """ - info = click.style(f'{Symbols.INFO} Device configuration = {os}:{codename}:{arch}', - fg=Colors.CYAN, bold=True) + info = click.style( + f"{Symbols.INFO} Device configuration = {os}:{codename}:{arch}", + fg=Colors.CYAN, + bold=True, + ) spinner.write(info) client = new_hwil_client() labels = prepare_device_labels_from_context(ctx) try: client.create_device(device_name, arch, os, codename, labels) - spinner.text = click.style(f'Device {device_name} created successfully.', fg=Colors.GREEN) + spinner.text = click.style( + f"Device {device_name} created successfully.", fg=Colors.GREEN + ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style(f'Failed to create device: {str(e)}', fg=Colors.RED) + spinner.text = click.style(f"Failed to create device: {str(e)}", fg=Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) def prepare_device_labels_from_context(ctx: click.Context) -> typing.Dict: - user_email = ctx.obj.data.get('email_id', '') + user_email = ctx.obj.data.get("email_id", "") if user_email: - user_email = user_email.split('@')[0] + user_email = user_email.split("@")[0] return { "user": user_email, - "organization": ctx.obj.data.get('organization_id', ''), - "project": ctx.obj.data.get('project_id', ''), + "organization": ctx.obj.data.get("organization_id", ""), + "project": ctx.obj.data.get("project_id", ""), } diff --git a/riocli/hwil/delete.py b/riocli/hwil/delete.py index 26806e46..709ca8ad 100644 --- a/riocli/hwil/delete.py +++ b/riocli/hwil/delete.py @@ -23,19 +23,26 @@ @click.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('devices', type=str, nargs=-1) -@click.option('--force', '-f', '--silent', 'force', is_flag=True, - default=False, help='Skip confirmation') -@with_spinner(text='Deleting device(s)...') +@click.argument("devices", type=str, nargs=-1) +@click.option( + "--force", + "-f", + "--silent", + "force", + is_flag=True, + default=False, + help="Skip confirmation", +) +@with_spinner(text="Deleting device(s)...") def delete_device( - devices: typing.List, - force: bool, - spinner: Yaspin = None, + devices: typing.List, + force: bool, + spinner: Yaspin = None, ) -> None: """Delete one or more devices. @@ -58,7 +65,7 @@ def delete_device( $ rio hwil delete my-device1 my-device2 my-device3 """ if not devices: - spinner.text = click.style('No device names provided', fg=Colors.RED) + spinner.text = click.style("No device names provided", fg=Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) @@ -68,30 +75,33 @@ def delete_device( try: fetched = client.list_devices() except Exception as e: - spinner.text = click.style(f'Error fetching device(s): {str(e)}', fg=Colors.RED) + spinner.text = click.style(f"Error fetching device(s): {str(e)}", fg=Colors.RED) spinner.red.fail(Symbols.ERROR) device_name_map = {name: None for name in devices} - final = {d['id']: d['name'] for d in fetched - if d['name'] in device_name_map} + final = {d["id"]: d["name"] for d in fetched if d["name"] in device_name_map} if not final: - spinner.text = click.style(f'No devices found with name(s): {", ".join(devices)}', fg=Colors.RED) + spinner.text = click.style( + f'No devices found with name(s): {", ".join(devices)}', fg=Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) with spinner.hidden(): if not force: - click.confirm(f'Do you want to delete {", ".join(final.values())}?', abort=True) + click.confirm( + f'Do you want to delete {", ".join(final.values())}?', abort=True + ) try: for device_id, device_name in final.items(): - spinner.text = f'Deleting device {device_name}...' + spinner.text = f"Deleting device {device_name}..." client.delete_device(device_id) - spinner.text = click.style(f'Device(s) deleted successfully!', fg=Colors.GREEN) + spinner.text = click.style("Device(s) deleted successfully!", fg=Colors.GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style(f'Error deleting device(s): {str(e)}', fg=Colors.RED) + spinner.text = click.style(f"Error deleting device(s): {str(e)}", fg=Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/hwil/execute.py b/riocli/hwil/execute.py index a02293a1..ebe6c228 100644 --- a/riocli/hwil/execute.py +++ b/riocli/hwil/execute.py @@ -22,13 +22,13 @@ @click.command( - 'execute', + "execute", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('device-name', required=True, type=str) -@click.argument('command', required=True, type=str) +@click.argument("device-name", required=True, type=str) +@click.argument("command", required=True, type=str) @name_to_id def execute(device_name: str, device_id: str, command: str) -> None: """Execute a command on a hardware-in-the-loop device. diff --git a/riocli/hwil/inspect.py b/riocli/hwil/inspect.py index 3ca188ed..8a672673 100644 --- a/riocli/hwil/inspect.py +++ b/riocli/hwil/inspect.py @@ -23,20 +23,21 @@ @click.command( - 'inspect', + "inspect", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--format', '-f', 'format_type', default='yaml', - type=click.Choice(['json', 'yaml'], case_sensitive=False)) -@click.argument('device-name', type=str) +@click.option( + "--format", + "-f", + "format_type", + default="yaml", + type=click.Choice(["json", "yaml"], case_sensitive=False), +) +@click.argument("device-name", type=str) @name_to_id -def inspect_device( - format_type: str, - device_name: str, - device_id: str -) -> None: +def inspect_device(format_type: str, device_name: str, device_id: str) -> None: """Print the details of a hardware-in-the-loop device.""" client = new_hwil_client() diff --git a/riocli/hwil/list.py b/riocli/hwil/list.py index 429c046f..bd2d6342 100644 --- a/riocli/hwil/list.py +++ b/riocli/hwil/list.py @@ -22,7 +22,7 @@ @click.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, @@ -41,8 +41,10 @@ def list_devices() -> None: def _display_device_list(devices: typing.List[dict], show_header: bool = True) -> None: headers = [] if show_header: - headers = ('ID', 'Name', 'Status', 'Static IP', 'Dynamic IP', 'Flavor') + headers = ("ID", "Name", "Status", "Static IP", "Dynamic IP", "Flavor") - data = [[d.id, d.name, d.status, d.static_ip, d.ip_address, d.flavor] for d in devices] + data = [ + [d.id, d.name, d.status, d.static_ip, d.ip_address, d.flavor] for d in devices + ] tabulate_data(data, headers) diff --git a/riocli/hwil/login.py b/riocli/hwil/login.py index 130804d1..e855cb95 100644 --- a/riocli/hwil/login.py +++ b/riocli/hwil/login.py @@ -24,26 +24,33 @@ from riocli.utils.context import get_root_context from riocli.utils.spinner import with_spinner -HWIL_LOGIN_SUCCESS = click.style('{} Successfully logged into HWIL!'.format(Symbols.SUCCESS), fg=Colors.GREEN) +HWIL_LOGIN_SUCCESS = click.style( + "{} Successfully logged into HWIL!".format(Symbols.SUCCESS), fg=Colors.GREEN +) @click.command( - 'login', + "login", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--username', help='Username for HWIL API') -@click.option('--password', help='Password for HWIL API') -@click.option('--interactive/--no-interactive', '--interactive/--silent', - is_flag=True, type=bool, default=True, - help='Make login interactive') +@click.option("--username", help="Username for HWIL API") +@click.option("--password", help="Password for HWIL API") +@click.option( + "--interactive/--no-interactive", + "--interactive/--silent", + is_flag=True, + type=bool, + default=True, + help="Make login interactive", +) @click.pass_context def login( - ctx: click.Context, - username: str, - password: str, - interactive: bool = True, + ctx: click.Context, + username: str, + password: str, + interactive: bool = True, ) -> None: """Authenticate with HWIL API. @@ -57,15 +64,15 @@ def login( ctx = get_root_context(ctx) if interactive: - username = username or click.prompt('Username') - password = password or click.prompt('Password', hide_input=True) + username = username or click.prompt("Username") + password = password or click.prompt("Password", hide_input=True) if not username: - click.secho(f'{Symbols.ERROR} Username not specified', fg=Colors.RED) + click.secho(f"{Symbols.ERROR} Username not specified", fg=Colors.RED) raise SystemExit(1) if not password: - click.secho(f'{Symbols.ERROR} Password not specified', fg=Colors.RED) + click.secho(f"{Symbols.ERROR} Password not specified", fg=Colors.RED) raise SystemExit(1) try: @@ -74,31 +81,28 @@ def login( raise SystemExit(1) from e -@with_spinner(text='Validating credentials...') +@with_spinner(text="Validating credentials...") def validate_and_set_hwil_token( - ctx: click.Context, - username: str, - password: str, - spinner=None + ctx: click.Context, username: str, password: str, spinner=None ) -> None: """Validates an auth token.""" - if 'environment' in ctx.obj.data: - os.environ['RIO_CONFIG'] = ctx.obj.filepath + if "environment" in ctx.obj.data: + os.environ["RIO_CONFIG"] = ctx.obj.filepath - token = b64encode(f"{username}:{password}".encode('utf-8')).decode("ascii") + token = b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii") client = HwilClient(auth_token=token) try: client.list_devices() - ctx.obj.data['hwil_auth_token'] = token + ctx.obj.data["hwil_auth_token"] = token ctx.obj.save() - spinner.text = click.style('Successfully logged in.', fg=Colors.GREEN) + spinner.text = click.style("Successfully logged in.", fg=Colors.GREEN) spinner.green.ok(Symbols.SUCCESS) except UnauthorizedError as e: spinner.red.text = click.style("Incorrect credentials.", fg=Colors.RED) spinner.red.fail(Symbols.ERROR) raise e except Exception as e: - spinner.text = click.style(f'Failed to login: {str(e)}', fg=Colors.RED) + spinner.text = click.style(f"Failed to login: {str(e)}", fg=Colors.RED) spinner.red.fail(Symbols.ERROR) raise e diff --git a/riocli/hwil/ssh.py b/riocli/hwil/ssh.py index ffa9cb82..2554b569 100644 --- a/riocli/hwil/ssh.py +++ b/riocli/hwil/ssh.py @@ -22,12 +22,12 @@ @click.command( - 'ssh', + "ssh", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('device-name', required=True, type=str) +@click.argument("device-name", required=True, type=str) @name_to_id def ssh(device_name: str, device_id: str, spinner=None) -> None: """SSH into a hardware-in-the-loop device. @@ -41,11 +41,17 @@ def ssh(device_name: str, device_id: str, spinner=None) -> None: try: device = new_hwil_client().get_device(device_id) - if not device.get('static_ip'): - click.secho(f'{Symbols.ERROR} Device does not have a static IP address', fg=Colors.RED) + if not device.get("static_ip"): + click.secho( + f"{Symbols.ERROR} Device does not have a static IP address", + fg=Colors.RED, + ) raise SystemExit(1) - click.secho(f'{Symbols.INFO} Enter this password when prompted: {device.password}', fg=Colors.BRIGHT_CYAN) + click.secho( + f"{Symbols.INFO} Enter this password when prompted: {device.password}", + fg=Colors.BRIGHT_CYAN, + ) os.system(f'ssh {device.username}@{device["static_ip"]}') except Exception as e: click.secho(str(e), fg=Colors.RED) diff --git a/riocli/hwil/util.py b/riocli/hwil/util.py index 180bc52d..d0d104b6 100644 --- a/riocli/hwil/util.py +++ b/riocli/hwil/util.py @@ -33,7 +33,7 @@ def decorated(**kwargs: typing.Any): click.secho(str(e), fg=Colors.RED) raise SystemExit(1) - name = kwargs.pop('device_name') + name = kwargs.pop("device_name") # device_name is not specified if name is None: @@ -48,8 +48,8 @@ def decorated(**kwargs: typing.Any): click.secho(str(e), fg=Colors.RED) raise SystemExit(1) - kwargs['device_name'] = name - kwargs['device_id'] = guid + kwargs["device_name"] = name + kwargs["device_id"] = guid f(**kwargs) return decorated @@ -73,7 +73,9 @@ def find_device_id(client: Client, name: str) -> str: raise DeviceNotFound(message="HWIL device not found") -def execute_command(client: Client, device_id: int, command: str) -> typing.Tuple[int, str, str]: +def execute_command( + client: Client, device_id: int, command: str +) -> typing.Tuple[int, str, str]: """Executes a command and waits for it to complete.""" try: response = client.execute_command(device_id, command) @@ -81,7 +83,7 @@ def execute_command(client: Client, device_id: int, command: str) -> typing.Tupl raise e try: - while response.status == 'PENDING': + while response.status == "PENDING": response = client.get_command(response.uuid) time.sleep(1) except Exception as e: @@ -89,4 +91,4 @@ def execute_command(client: Client, device_id: int, command: str) -> typing.Tupl o = response.result.output.pop() - return o.get('rc'), o.get('stdout'), o.get('stderr') + return o.get("rc"), o.get("stdout"), o.get("stderr") diff --git a/riocli/hwilclient/__init__.py b/riocli/hwilclient/__init__.py index ae8c6492..d65eea00 100644 --- a/riocli/hwilclient/__init__.py +++ b/riocli/hwilclient/__init__.py @@ -1 +1 @@ -from riocli.hwilclient.client import Client +from riocli.hwilclient.client import Client as Client diff --git a/riocli/hwilclient/client.py b/riocli/hwilclient/client.py index e83d2ce4..eba5982e 100644 --- a/riocli/hwilclient/client.py +++ b/riocli/hwilclient/client.py @@ -30,34 +30,35 @@ def handle_server_errors(response: requests.Response): # 409 Conflict if status_code == http.HTTPStatus.CONFLICT: - raise ConflictError('already exists') + raise ConflictError("already exists") # 401 Unauthorized if status_code == http.HTTPStatus.UNAUTHORIZED: - raise UnauthorizedError('unauthorized access') + raise UnauthorizedError("unauthorized access") # 500 Internal Server Error if status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR: - raise Exception('internal server error') + raise Exception("internal server error") # 501 Not Implemented if status_code == http.HTTPStatus.NOT_IMPLEMENTED: - raise Exception('not implemented') + raise Exception("not implemented") # 502 Bad Gateway if status_code == http.HTTPStatus.BAD_GATEWAY: - raise Exception('bad gateway') + raise Exception("bad gateway") # 503 Service Unavailable if status_code == http.HTTPStatus.SERVICE_UNAVAILABLE: - raise Exception('service unavailable') + raise Exception("service unavailable") # 504 Gateway Timeout if status_code == http.HTTPStatus.GATEWAY_TIMEOUT: - raise Exception('gateway timeout') + raise Exception("gateway timeout") # Anything else that is not known if status_code > 504: - raise Exception('unknown server error') + raise Exception("unknown server error") class Client(object): """ HWILv3 API Client """ + HWIL_URL = "https://hwilv3.rapyuta.io" ARCH_OS_DICT = { "amd64": { @@ -68,13 +69,9 @@ class Client(object): } }, "arm64": { - "ubuntu": { - "focal": "ubuntu-focal-ros-noetic-py3" - }, - "debian": { - "bullseye": "debian-bullseye-docker" - } - } + "ubuntu": {"focal": "ubuntu-focal-ros-noetic-py3"}, + "debian": {"bullseye": "debian-bullseye-docker"}, + }, } def __init__(self, auth_token: str): @@ -82,12 +79,12 @@ def __init__(self, auth_token: str): self._host = self.HWIL_URL def create_device( - self: Client, - name: str, - arch: str, - os: str, - codename: str, - labels: dict = None, + self: Client, + name: str, + arch: str, + os: str, + codename: str, + labels: dict = None, ) -> Munch: """Create a HWIL device.""" url = f"{self._host}/device/" @@ -109,11 +106,15 @@ def create_device( "name": name, "architecture": arch, "labels": sanitized_labels, - "flavor": self.ARCH_OS_DICT.get(arch).get(os).get(codename) + "flavor": self.ARCH_OS_DICT.get(arch).get(os).get(codename), } - response = RestClient(url).method(HttpMethod.POST).headers( - headers).execute(payload=payload) + response = ( + RestClient(url) + .method(HttpMethod.POST) + .headers(headers) + .execute(payload=payload) + ) handle_server_errors(response) @@ -156,8 +157,12 @@ def execute_command(self: Client, device_id: int, command: str) -> Munch: "uuid": generate_short_guid(), } - response = RestClient(url).method(HttpMethod.POST).headers( - headers).execute(payload=payload) + response = ( + RestClient(url) + .method(HttpMethod.POST) + .headers(headers) + .execute(payload=payload) + ) handle_server_errors(response) @@ -180,7 +185,9 @@ def get_command(self: Client, command_uuid: str) -> Munch: return munchify(data) - def poll_till_device_ready(self: Client, device_id: int, sleep_interval: int, retry_limit: int) -> None: + def poll_till_device_ready( + self: Client, device_id: int, sleep_interval: int, retry_limit: int + ) -> None: """Poll until HWIL device is ready""" url = f"{self._host}/device/{device_id}" headers = self._get_auth_header() @@ -195,13 +202,13 @@ def poll_till_device_ready(self: Client, device_id: int, sleep_interval: int, re raise Exception("hwil: {}".format(response.text)) device = munchify(data) - if device.status != 'IDLE': + if device.status != "IDLE": time.sleep(sleep_interval) continue return - msg = f'Retries exhausted: Tried {retry_limit} times with {sleep_interval}s interval.' + msg = f"Retries exhausted: Tried {retry_limit} times with {sleep_interval}s interval." raise RetriesExhausted(msg) def list_devices(self: Client): diff --git a/riocli/jsonschema/validate.py b/riocli/jsonschema/validate.py index 55fa3bb2..6ba70d97 100644 --- a/riocli/jsonschema/validate.py +++ b/riocli/jsonschema/validate.py @@ -32,12 +32,16 @@ def set_defaults(validator, properties, instance, schema): i.setdefault(p, sub_schema["default"]) for error in validate_properties( - validator, properties, instance, schema, + validator, + properties, + instance, + schema, ): yield error return validators.extend( - validator_class, {"properties": set_defaults}, + validator_class, + {"properties": set_defaults}, ) @@ -49,6 +53,6 @@ def load_schema(resource: str) -> DefaultValidator: """ Reads JSON schema yaml and returns a validator. """ - schema_dir = Path(__file__).parent.joinpath('schemas') + schema_dir = Path(__file__).parent.joinpath("schemas") with open(schema_dir.joinpath(resource + "-schema.yaml")) as f: return DefaultValidator(yaml.safe_load(f)) diff --git a/riocli/managedservice/__init__.py b/riocli/managedservice/__init__.py index c15cd8a6..b4816b0e 100644 --- a/riocli/managedservice/__init__.py +++ b/riocli/managedservice/__init__.py @@ -23,8 +23,8 @@ @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color="yellow", + help_options_color="green", hidden=True, ) def managedservice() -> None: diff --git a/riocli/managedservice/delete.py b/riocli/managedservice/delete.py index 94cb9383..2bdafc70 100644 --- a/riocli/managedservice/delete.py +++ b/riocli/managedservice/delete.py @@ -17,8 +17,8 @@ from riocli.config import new_v2_client -@click.command('delete') -@click.argument('instance-name', required=True) +@click.command("delete") +@click.argument("instance-name", required=True) def delete_instance(instance_name: str): """ Delete a managedservice instance @@ -26,8 +26,8 @@ def delete_instance(instance_name: str): try: client = new_v2_client() r = client.delete_instance(instance_name) - if r.get('success', None): - click.secho("✅ Deleted instance '{}'".format(instance_name), fg='green') + if r.get("success", None): + click.secho("✅ Deleted instance '{}'".format(instance_name), fg="green") except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) diff --git a/riocli/managedservice/inspect.py b/riocli/managedservice/inspect.py index 50136a42..aca08a06 100644 --- a/riocli/managedservice/inspect.py +++ b/riocli/managedservice/inspect.py @@ -19,10 +19,15 @@ from riocli.utils import inspect_with_format -@click.command('inspect') -@click.option('--format', '-f', 'format_type', default='yaml', - type=click.Choice(['json', 'yaml'], case_sensitive=False)) -@click.argument('instance-name', required=True) +@click.command("inspect") +@click.option( + "--format", + "-f", + "format_type", + default="yaml", + type=click.Choice(["json", "yaml"], case_sensitive=False), +) +@click.argument("instance-name", required=True) def inspect_instance(format_type: str, instance_name: str): """ Inspect a managedservice instance @@ -32,5 +37,5 @@ def inspect_instance(format_type: str, instance_name: str): instance = client.get_instance(instance_name) inspect_with_format(unmunchify(instance), format_type) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) diff --git a/riocli/managedservice/list.py b/riocli/managedservice/list.py index c1a4488f..58373901 100644 --- a/riocli/managedservice/list.py +++ b/riocli/managedservice/list.py @@ -20,7 +20,7 @@ from riocli.utils import tabulate_data -@click.command('list') +@click.command("list") def list_instances(): """ List all the managedservice instances @@ -30,7 +30,7 @@ def list_instances(): instances = client.list_instances() _display_instances(instances) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) diff --git a/riocli/managedservice/list_providers.py b/riocli/managedservice/list_providers.py index 7ea22553..3fa97ec2 100644 --- a/riocli/managedservice/list_providers.py +++ b/riocli/managedservice/list_providers.py @@ -19,7 +19,7 @@ from riocli.utils import tabulate_data -@click.command('providers') +@click.command("providers") def list_providers(): """ List available managedservice providers @@ -29,16 +29,16 @@ def list_providers(): providers = client.list_providers() _display_providers(providers) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) def _display_providers(providers: typing.Any): - headers = ['Provider Name'] + headers = ["Provider Name"] data = [] for provider in providers: - if provider.name == 'dummy': + if provider.name == "dummy": continue data.append([provider.name]) diff --git a/riocli/model/__init__.py b/riocli/model/__init__.py index 24985462..cb62e6be 100644 --- a/riocli/model/__init__.py +++ b/riocli/model/__init__.py @@ -1 +1 @@ -from riocli.model.base import Model +from riocli.model.base import Model as Model diff --git a/riocli/model/base.py b/riocli/model/base.py index 42fbd07d..d2a634d0 100644 --- a/riocli/model/base.py +++ b/riocli/model/base.py @@ -19,7 +19,7 @@ from riocli.constants import ApplyResult from riocli.jsonschema.validate import load_schema -DELETE_POLICY_LABEL = 'rapyuta.io/deletionPolicy' +DELETE_POLICY_LABEL = "rapyuta.io/deletionPolicy" class Model(ABC, Munch): @@ -41,6 +41,7 @@ def delete(self, *args, **kwargs): by the subclasses. It validates the model against the corresponding schema that are defined in the schema files. """ + @abstractmethod def apply(self, *args, **kwargs) -> ApplyResult: """Create or update the object. @@ -62,14 +63,14 @@ def delete(self, *args, **kwargs) -> None: @classmethod def validate(cls, d: typing.Dict) -> None: """Validate the model against the corresponding schema.""" - kind = d.get('kind') + kind = d.get("kind") if not kind: - raise ValueError('kind is required') + raise ValueError("kind is required") # StaticRoute's schema file is named # static_route-schema.yaml. - if kind == 'StaticRoute': - kind = 'static_route' + if kind == "StaticRoute": + kind = "static_route" schema = load_schema(kind.lower()) schema.validate(d) diff --git a/riocli/network/delete.py b/riocli/network/delete.py index 858dc6ec..72807e7e 100644 --- a/riocli/network/delete.py +++ b/riocli/network/delete.py @@ -29,24 +29,29 @@ @click.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--force', '-f', is_flag=True, default=False, - help='Skip confirmation', type=bool) -@click.option('--workers', '-w', - help="Number of parallel workers while running deleting networks. Defaults to 10", - type=int, default=10) -@click.argument('network-name-or-regex', type=str) -@with_spinner(text='Deleting network...') +@click.option( + "--force", "-f", is_flag=True, default=False, help="Skip confirmation", type=bool +) +@click.option( + "--workers", + "-w", + help="Number of parallel workers while running deleting networks. Defaults to 10", + type=int, + default=10, +) +@click.argument("network-name-or-regex", type=str) +@with_spinner(text="Deleting network...") def delete_network( - force: bool, - network_name_or_regex: str, - delete_all: bool = False, - workers: int = 10, - spinner: Yaspin = None + force: bool, + network_name_or_regex: str, + delete_all: bool = False, + workers: int = 10, + spinner: Yaspin = None, ) -> None: """Delete one or more networks with a name or a regex pattern. @@ -87,8 +92,7 @@ def delete_network( try: networks = fetch_networks(client, network_name_or_regex, "", delete_all) except Exception as e: - spinner.text = click.style( - 'Failed to find network(s): {}'.format(e), Colors.RED) + spinner.text = click.style("Failed to find network(s): {}".format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -100,17 +104,18 @@ def delete_network( with spinner.hidden(): print_networks_for_confirmation(networks) - spinner.write('') + spinner.write("") if not force: with spinner.hidden(): - click.confirm('Do you want to delete the above network(s)?', default=True, abort=True) + click.confirm( + "Do you want to delete the above network(s)?", default=True, abort=True + ) try: f = functools.partial(_apply_delete, client) result = apply_func_with_result( - f=f, items=networks, - workers=workers, key=lambda x: x[0] + f=f, items=networks, workers=workers, key=lambda x: x[0] ) data, statuses = [], [] for name, status, msg in result: @@ -118,18 +123,17 @@ def delete_network( icon = Symbols.SUCCESS if status else Symbols.ERROR statuses.append(status) - data.append([ - click.style(name, fg), - click.style('{} {}'.format(icon, msg), fg) - ]) + data.append( + [click.style(name, fg), click.style("{} {}".format(icon, msg), fg)] + ) with spinner.hidden(): - tabulate_data(data, headers=['Name', 'Status']) + tabulate_data(data, headers=["Name", "Status"]) # When no network is deleted, raise an exception. if not any(statuses): - spinner.write('') - spinner.text = click.style('Failed to delete network(s).', Colors.RED) + spinner.write("") + spinner.text = click.style("Failed to delete network(s).", Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) @@ -137,12 +141,12 @@ def delete_network( fg = Colors.GREEN if all(statuses) else Colors.YELLOW text = "successfully" if all(statuses) else "partially" - spinner.text = click.style( - 'Networks(s) deleted {}.'.format(text), fg) + spinner.text = click.style("Networks(s) deleted {}.".format(text), fg) spinner.ok(click.style(icon, fg)) except Exception as e: spinner.text = click.style( - 'Failed to delete network(s): {}'.format(e), Colors.RED) + "Failed to delete network(s): {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -150,6 +154,6 @@ def delete_network( def _apply_delete(client: Client, result: Queue, network: Network) -> None: try: client.delete_network(network_name=network.metadata.name) - result.put((network.metadata.name, True, 'Network Deleted Successfully')) + result.put((network.metadata.name, True, "Network Deleted Successfully")) except Exception as e: result.put((network.metadata.name, False, str(e))) diff --git a/riocli/network/inspect.py b/riocli/network/inspect.py index 9209d626..bcd2f94c 100644 --- a/riocli/network/inspect.py +++ b/riocli/network/inspect.py @@ -19,10 +19,15 @@ from riocli.utils import inspect_with_format -@click.command('inspect') -@click.option('--format', '-f', 'format_type', default='yaml', - type=click.Choice(['json', 'yaml'], case_sensitive=False)) -@click.argument('network-name') +@click.command("inspect") +@click.option( + "--format", + "-f", + "format_type", + default="yaml", + type=click.Choice(["json", "yaml"], case_sensitive=False), +) +@click.argument("network-name") def inspect_network(format_type: str, network_name: str) -> None: """Print the details of a network. @@ -34,7 +39,7 @@ def inspect_network(format_type: str, network_name: str) -> None: network_obj = client.get_network(network_name) if not network_obj: - click.secho("network not found", fg='red') + click.secho("network not found", fg="red") raise SystemExit(1) inspect_with_format(unmunchify(network_obj), format_type) diff --git a/riocli/network/list.py b/riocli/network/list.py index 1ea760ca..42f4f838 100644 --- a/riocli/network/list.py +++ b/riocli/network/list.py @@ -23,15 +23,26 @@ @click.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--network', help='Type of Network', - type=click.Choice(['routed', 'native', 'both']), default='both') -@click.option('--label', '-l', 'labels', multiple=True, type=click.STRING, - default=(), help='Filter the deployment list by labels') +@click.option( + "--network", + help="Type of Network", + type=click.Choice(["routed", "native", "both"]), + default="both", +) +@click.option( + "--label", + "-l", + "labels", + multiple=True, + type=click.STRING, + default=(), + help="Filter the deployment list by labels", +) def list_networks(network: str, labels: typing.List[str]) -> None: """List the networks in the current project. @@ -47,7 +58,7 @@ def list_networks(network: str, labels: typing.List[str]) -> None: try: client = new_v2_client(with_project=True) - query = {'labelSelector': labels} + query = {"labelSelector": labels} if network not in ("both", ""): query.update({"networkType": network}) @@ -55,17 +66,17 @@ def list_networks(network: str, labels: typing.List[str]) -> None: networks = sorted(networks, key=lambda n: n.metadata.name.lower()) _display_network_list(networks, show_header=True) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) def _display_network_list( - networks: typing.List[Munch], - show_header: bool = True, + networks: typing.List[Munch], + show_header: bool = True, ) -> None: headers = [] if show_header: - headers = ('Network ID', 'Network Name', 'Runtime', 'Type', 'Phase', 'Status') + headers = ("Network ID", "Network Name", "Runtime", "Type", "Phase", "Status") data = [] for network in networks: phase = network.status.phase if network.status else "" @@ -73,6 +84,14 @@ def _display_network_list( network_type = network.spec.type data.append( - [network.metadata.guid, network.metadata.name, network.spec.runtime, network_type, phase, status]) + [ + network.metadata.guid, + network.metadata.name, + network.spec.runtime, + network_type, + phase, + status, + ] + ) tabulate_data(data, headers) diff --git a/riocli/network/model.py b/riocli/network/model.py index afb82fd1..1cab300f 100644 --- a/riocli/network/model.py +++ b/riocli/network/model.py @@ -32,12 +32,14 @@ def apply(self, *args, **kwargs) -> None: self.metadata.createdAt = None self.metadata.updatedAt = None - retry_count = int(kwargs.get('retry_count')) - retry_interval = int(kwargs.get('retry_interval')) + retry_count = int(kwargs.get("retry_count")) + retry_interval = int(kwargs.get("retry_interval")) try: r = client.create_network(unmunchify(self)) - client.poll_network(r.metadata.name, retry_count=retry_count, sleep_interval=retry_interval) + client.poll_network( + r.metadata.name, retry_count=retry_count, sleep_interval=retry_interval + ) return ApplyResult.CREATED except HttpAlreadyExistsError: return ApplyResult.EXISTS diff --git a/riocli/network/util.py b/riocli/network/util.py index 1a4bfdcf..e84cd73e 100644 --- a/riocli/network/util.py +++ b/riocli/network/util.py @@ -11,24 +11,24 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import functools +import re from typing import List + from munch import Munch -import re -from riocli.utils import tabulate_data from riocli.network.model import Network +from riocli.utils import tabulate_data from riocli.v2client import Client def fetch_networks( - client: Client, - network_name_or_regex: str, - network_type: str, - include_all: bool, + client: Client, + network_name_or_regex: str, + network_type: str, + include_all: bool, ) -> List[Network]: if network_type: - networks = client.list_networks(query={'network_type': network_type}) + networks = client.list_networks(query={"network_type": network_type}) else: networks = client.list_networks() @@ -44,6 +44,6 @@ def fetch_networks( def print_networks_for_confirmation(networks: List[Munch]) -> None: - headers = ['Name', 'Type'] + headers = ["Name", "Type"] data = [[n.metadata.name, n.spec.type] for n in networks] tabulate_data(data, headers) diff --git a/riocli/organization/inspect.py b/riocli/organization/inspect.py index b072e19d..0dbcf303 100644 --- a/riocli/organization/inspect.py +++ b/riocli/organization/inspect.py @@ -23,20 +23,25 @@ @click.command( - 'inspect', + "inspect", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--format', '-f', 'format_type', default='yaml', - type=click.Choice(['json', 'yaml'], case_sensitive=False)) -@click.argument('organization-name', type=str) +@click.option( + "--format", + "-f", + "format_type", + default="yaml", + type=click.Choice(["json", "yaml"], case_sensitive=False), +) +@click.argument("organization-name", type=str) @name_to_organization_guid def inspect_organization( - format_type: str, - organization_name: str, - organization_guid: str, - organization_short_id: str, + format_type: str, + organization_name: str, + organization_guid: str, + organization_short_id: str, ) -> None: """Inspect an organization. @@ -54,20 +59,20 @@ def inspect_organization( def make_organization_inspectable(organization: typing.Dict) -> typing.Dict: creator = None - for user in organization['users']: - if user['guid'] == organization['creator']: - creator = user['emailID'] + for user in organization["users"]: + if user["guid"] == organization["creator"]: + creator = user["emailID"] break return { - 'name': organization['name'], - 'created_at': organization['CreatedAt'], - 'updated_at': organization['UpdatedAt'], - 'guid': organization['guid'], - 'url': organization['url'], - 'creator': creator, - 'short_guid': organization['shortGUID'], - 'state': organization['state'], - 'users': len(organization['users']), - 'country': organization['country']['code'], + "name": organization["name"], + "created_at": organization["CreatedAt"], + "updated_at": organization["UpdatedAt"], + "guid": organization["guid"], + "url": organization["url"], + "creator": creator, + "short_guid": organization["shortGUID"], + "state": organization["state"], + "users": len(organization["users"]), + "country": organization["country"]["code"], } diff --git a/riocli/organization/invite_user.py b/riocli/organization/invite_user.py index 23192c66..762e0a8f 100644 --- a/riocli/organization/invite_user.py +++ b/riocli/organization/invite_user.py @@ -22,12 +22,12 @@ @click.command( - 'invite-user', + "invite-user", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('user-email', type=str) +@click.argument("user-email", type=str) @click.pass_context def invite_user(ctx: click.Context, user_email: str) -> None: """Invite a new user to the current organization. @@ -48,12 +48,19 @@ def invite_user(ctx: click.Context, user_email: str) -> None: try: validate_email(user_email) except EmailNotValidError as e: - click.secho('{} {} is not a valid email address'.format(Symbols.ERROR, user_email), fg=Colors.RED) + click.secho( + "{} {} is not a valid email address".format(Symbols.ERROR, user_email), + fg=Colors.RED, + ) raise SystemExit(1) from e try: - invite_user_to_org(ctx.obj.data['organization_id'], user_email) - click.secho('{} User invited successfully.'.format(Symbols.SUCCESS), fg=Colors.GREEN) + invite_user_to_org(ctx.obj.data["organization_id"], user_email) + click.secho( + "{} User invited successfully.".format(Symbols.SUCCESS), fg=Colors.GREEN + ) except Exception as e: - click.secho('{} Failed to invite user: {}'.format(Symbols.ERROR, e), fg=Colors.RED) + click.secho( + "{} Failed to invite user: {}".format(Symbols.ERROR, e), fg=Colors.RED + ) raise SystemExit(1) from e diff --git a/riocli/organization/list.py b/riocli/organization/list.py index 9a5b4f9d..e676df1f 100644 --- a/riocli/organization/list.py +++ b/riocli/organization/list.py @@ -21,7 +21,7 @@ @click.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, @@ -36,7 +36,7 @@ def list_organizations(ctx: click.Context) -> None: try: client = new_client(with_project=False) organizations = client.get_user_organizations() - current = ctx.obj.data['organization_id'] + current = ctx.obj.data["organization_id"] print_organizations(organizations, current) except Exception as e: click.secho(str(e), fg=Colors.RED) @@ -54,10 +54,11 @@ def print_organizations(organizations, current): if org.guid == current: fg = Colors.GREEN bold = True - data.append([ - click.style(v, fg=fg, bold=bold) - for v in (org.name, org.guid, - org.creator, org.short_guid) - ]) + data.append( + [ + click.style(v, fg=fg, bold=bold) + for v in (org.name, org.guid, org.creator, org.short_guid) + ] + ) tabulate_data(data, headers) diff --git a/riocli/organization/remove_user.py b/riocli/organization/remove_user.py index 15e81748..7ee8b817 100644 --- a/riocli/organization/remove_user.py +++ b/riocli/organization/remove_user.py @@ -22,27 +22,33 @@ @click.command( - 'remove-user', + "remove-user", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('user-email', type=str) +@click.argument("user-email", type=str) @click.pass_context def remove_user(ctx: click.Context, user_email: str) -> None: - """Remove a user from the current organization - """ + """Remove a user from the current organization""" ctx = get_root_context(ctx) try: validate_email(user_email) except EmailNotValidError as e: - click.secho('{} {} is not a valid email address'.format(Symbols.ERROR, user_email), fg=Colors.RED) + click.secho( + "{} {} is not a valid email address".format(Symbols.ERROR, user_email), + fg=Colors.RED, + ) raise SystemExit(1) from e try: - remove_user_from_org(ctx.obj.data['organization_id'], user_email) - click.secho('{} User removed successfully.'.format(Symbols.SUCCESS), fg=Colors.GREEN) + remove_user_from_org(ctx.obj.data["organization_id"], user_email) + click.secho( + "{} User removed successfully.".format(Symbols.SUCCESS), fg=Colors.GREEN + ) except Exception as e: - click.secho('{} Failed to remove user: {}'.format(Symbols.ERROR, e), fg=Colors.RED) + click.secho( + "{} Failed to remove user: {}".format(Symbols.ERROR, e), fg=Colors.RED + ) raise SystemExit(1) from e diff --git a/riocli/organization/select.py b/riocli/organization/select.py index de5b9fd2..9bce5274 100644 --- a/riocli/organization/select.py +++ b/riocli/organization/select.py @@ -17,30 +17,35 @@ from click_help_colors import HelpColorsCommand from riocli.auth.util import select_project -from riocli.constants import Colors +from riocli.constants import Colors, Symbols from riocli.project.util import name_to_organization_guid from riocli.utils.context import get_root_context from riocli.vpn.util import cleanup_hosts_file @click.command( - 'select', + "select", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('organization-name', type=str) -@click.option('--interactive/--no-interactive', '--interactive/--silent', - is_flag=True, type=bool, default=True, - help='Make the selection interactive') +@click.argument("organization-name", type=str) +@click.option( + "--interactive/--no-interactive", + "--interactive/--silent", + is_flag=True, + type=bool, + default=True, + help="Make the selection interactive", +) @click.pass_context @name_to_organization_guid def select_organization( - ctx: click.Context, - organization_name: str, - organization_guid: str, - organization_short_id: str, - interactive: bool, + ctx: click.Context, + organization_name: str, + organization_guid: str, + organization_short_id: str, + interactive: bool, ) -> None: """Set the current organization. @@ -66,29 +71,36 @@ def select_organization( """ ctx = get_root_context(ctx) - if ctx.obj.data.get('organization_id') == organization_guid: - click.secho("You are already in the '{}' organization".format( - organization_name), fg=Colors.GREEN) + if ctx.obj.data.get("organization_id") == organization_guid: + click.secho( + "You are already in the '{}' organization".format(organization_name), + fg=Colors.GREEN, + ) return - ctx.obj.data['organization_id'] = organization_guid - ctx.obj.data['organization_name'] = organization_name - ctx.obj.data['organization_short_id'] = organization_short_id + ctx.obj.data["organization_id"] = organization_guid + ctx.obj.data["organization_name"] = organization_name + ctx.obj.data["organization_short_id"] = organization_short_id if sys.stdout.isatty() and interactive: select_project(ctx.obj, organization=organization_guid) else: - ctx.obj.data['project_id'] = "" - ctx.obj.data['project_name'] = "" + ctx.obj.data["project_id"] = "" + ctx.obj.data["project_name"] = "" click.secho( "Your organization has been set to '{}'\n" - "Please set your project with `rio project select PROJECT_NAME`".format(organization_name), - fg=Colors.GREEN) + "Please set your project with `rio project select PROJECT_NAME`".format( + organization_name + ), + fg=Colors.GREEN, + ) ctx.obj.save() try: cleanup_hosts_file() except Exception as e: - click.secho(f'{Symbols.WARNING} Failed to ' - f'clean up hosts file: {str(e)}', fg=Colors.YELLOW) + click.secho( + f"{Symbols.WARNING} Failed to " f"clean up hosts file: {str(e)}", + fg=Colors.YELLOW, + ) diff --git a/riocli/organization/users.py b/riocli/organization/users.py index 0e4fa006..6683565d 100644 --- a/riocli/organization/users.py +++ b/riocli/organization/users.py @@ -22,7 +22,7 @@ @click.command( - 'users', + "users", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, @@ -31,24 +31,26 @@ def list_users(ctx: click.Context) -> None: """Lists all users in the organization.""" ctx = get_root_context(ctx) - current_user_email = ctx.obj.data.get('email_id') + current_user_email = ctx.obj.data.get("email_id") try: - organization = get_organization_details(ctx.obj.data['organization_id']) + organization = get_organization_details(ctx.obj.data["organization_id"]) except Exception as e: - click.secho('{} Failed to get organization details'.format(Symbols.ERROR), fg=Colors.RED) + click.secho( + "{} Failed to get organization details".format(Symbols.ERROR), fg=Colors.RED + ) raise SystemExit(1) from e - users = organization.get('users') - users.sort(key=lambda u: u['emailID']) + users = organization.get("users") + users.sort(key=lambda u: u["emailID"]) data = [] for u in users: fg, bold = None, False - if u['emailID'] == current_user_email: + if u["emailID"] == current_user_email: fg, bold = Colors.GREEN, True - full_name = '{} {}'.format(u.get('firstName', ''), u.get('lastName', '')) - row = [u['guid'], full_name, u['emailID'], u['state']] + full_name = "{} {}".format(u.get("firstName", ""), u.get("lastName", "")) + row = [u["guid"], full_name, u["emailID"], u["state"]] data.append([click.style(v, fg=fg, bold=bold) for v in row]) - tabulate_data(data, headers=['GUID', 'Name', 'EmailID', 'Status']) + tabulate_data(data, headers=["GUID", "Name", "EmailID", "Status"]) diff --git a/riocli/organization/utils.py b/riocli/organization/utils.py index eee98ff3..6bda6d32 100644 --- a/riocli/organization/utils.py +++ b/riocli/organization/utils.py @@ -20,24 +20,22 @@ def _api_call( - method: str, - path: typing.Union[str, None] = None, - payload: typing.Union[typing.Dict, None] = None, - load_response: bool = True, + method: str, + path: typing.Union[str, None] = None, + payload: typing.Union[typing.Dict, None] = None, + load_response: bool = True, ) -> typing.Dict: config = Configuration() coreapi_host = config.data.get( - 'core_api_host', - 'https://gaapiserver.apps.okd4v2.prod.rapyuta.io' + "core_api_host", "https://gaapiserver.apps.okd4v2.prod.rapyuta.io" ) - url = '{}/api/organization'.format(coreapi_host) + url = "{}/api/organization".format(coreapi_host) if path: - url = '{}/{}'.format(url, path) + url = "{}/{}".format(url, path) headers = config.get_auth_header() - response = RestClient(url).method(method).headers(headers).execute( - payload=payload) + response = RestClient(url).method(method).headers(headers).execute(payload=payload) data = None @@ -45,21 +43,25 @@ def _api_call( data = response.json() if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception(err_msg) return data def get_organization_details(organization_guid: str) -> typing.Dict: - return _api_call(HttpMethod.GET, '{}/get'.format(organization_guid)) + return _api_call(HttpMethod.GET, "{}/get".format(organization_guid)) def invite_user_to_org(organization_guid: str, user_email: str) -> typing.Dict: - payload = {'userEmail': user_email} - return _api_call(HttpMethod.PUT, '{}/adduser'.format(organization_guid), payload=payload) + payload = {"userEmail": user_email} + return _api_call( + HttpMethod.PUT, "{}/adduser".format(organization_guid), payload=payload + ) def remove_user_from_org(organization_guid: str, user_email: str) -> typing.Dict: - payload = {'userEmail': user_email} - return _api_call(HttpMethod.DELETE, '{}/removeuser'.format(organization_guid), payload=payload) + payload = {"userEmail": user_email} + return _api_call( + HttpMethod.DELETE, "{}/removeuser".format(organization_guid), payload=payload + ) diff --git a/riocli/package/__init__.py b/riocli/package/__init__.py index 5520b8c6..fabf404c 100644 --- a/riocli/package/__init__.py +++ b/riocli/package/__init__.py @@ -23,8 +23,8 @@ @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color="yellow", + help_options_color="green", ) def package() -> None: """ diff --git a/riocli/package/delete.py b/riocli/package/delete.py index 5e816861..0318d340 100644 --- a/riocli/package/delete.py +++ b/riocli/package/delete.py @@ -27,25 +27,47 @@ from riocli.v2client import Client -@click.command('delete') -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, - type=click.BOOL, default=False, help="Skip confirmation") -@click.option('-a', '--all', 'delete_all', is_flag=True, default=False, - help='Deletes all packages in the project') -@click.option('--version', 'package_version', type=str, - help='Semantic version of the Package, only used when name is used instead of GUID') -@click.option('--workers', '-w', - help="Number of parallel workers while running deleting packages. Defaults to 10", - type=int, default=10) -@click.argument('package-name-or-regex', type=str, default="") +@click.command("delete") +@click.option( + "-f", + "--force", + "--silent", + "silent", + is_flag=True, + type=click.BOOL, + default=False, + help="Skip confirmation", +) +@click.option( + "-a", + "--all", + "delete_all", + is_flag=True, + default=False, + help="Deletes all packages in the project", +) +@click.option( + "--version", + "package_version", + type=str, + help="Semantic version of the Package, only used when name is used instead of GUID", +) +@click.option( + "--workers", + "-w", + help="Number of parallel workers while running deleting packages. Defaults to 10", + type=int, + default=10, +) +@click.argument("package-name-or-regex", type=str, default="") @with_spinner(text="Deleting package(s)...") def delete_package( - package_name_or_regex: str, - package_version: str, - silent: bool = False, - delete_all: bool = False, - workers: int = 10, - spinner: Yaspin = None, + package_name_or_regex: str, + package_version: str, + silent: bool = False, + delete_all: bool = False, + workers: int = 10, + spinner: Yaspin = None, ) -> None: """ Delete the package from the Platform @@ -58,10 +80,11 @@ def delete_package( return try: - packages = fetch_packages(client, package_name_or_regex, package_version, delete_all) + packages = fetch_packages( + client, package_name_or_regex, package_version, delete_all + ) except Exception as e: - spinner.text = click.style( - 'Failed to find package(s): {}'.format(e), Colors.RED) + spinner.text = click.style("Failed to find package(s): {}".format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -73,17 +96,18 @@ def delete_package( with spinner.hidden(): print_packages_for_confirmation(packages) - spinner.write('') + spinner.write("") if not silent: with spinner.hidden(): - click.confirm('Do you want to delete the above package(s)?', default=True, abort=True) + click.confirm( + "Do you want to delete the above package(s)?", default=True, abort=True + ) try: f = functools.partial(_apply_delete, client) result = apply_func_with_result( - f=f, items=packages, - workers=workers, key=lambda x: x[0] + f=f, items=packages, workers=workers, key=lambda x: x[0] ) data, statuses = [], [] @@ -91,18 +115,17 @@ def delete_package( fg = Colors.GREEN if status else Colors.RED icon = Symbols.SUCCESS if status else Symbols.ERROR statuses.append(status) - data.append([ - click.style(name, fg), - click.style('{} {}'.format(icon, msg), fg) - ]) + data.append( + [click.style(name, fg), click.style("{} {}".format(icon, msg), fg)] + ) with spinner.hidden(): - tabulate_data(data, headers=['Name', 'Status']) + tabulate_data(data, headers=["Name", "Status"]) # When no package is deleted, raise an exception. if not any(statuses): - spinner.write('') - spinner.text = click.style('Failed to delete package(s).', Colors.RED) + spinner.write("") + spinner.text = click.style("Failed to delete package(s).", Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) @@ -110,12 +133,12 @@ def delete_package( fg = Colors.GREEN if all(statuses) else Colors.YELLOW text = "successfully" if all(statuses) else "partially" - spinner.text = click.style( - 'Package(s) deleted {}.'.format(text), fg) + spinner.text = click.style("Package(s) deleted {}.".format(text), fg) spinner.ok(click.style(icon, fg)) except Exception as e: spinner.text = click.style( - 'Failed to delete package(s): {}'.format(e), Colors.RED) + "Failed to delete package(s): {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -123,7 +146,10 @@ def delete_package( def _apply_delete(client: Client, result: Queue, package: Package) -> None: name_version = "{}@{}".format(package.metadata.name, package.metadata.version) try: - client.delete_package(package_name=package.metadata.name, query={"version": package.metadata.version}) - result.put((name_version, True, 'Package deleted successfully')) + client.delete_package( + package_name=package.metadata.name, + query={"version": package.metadata.version}, + ) + result.put((name_version, True, "Package deleted successfully")) except Exception as e: result.put((name_version, False, str(e))) diff --git a/riocli/package/deployment.py b/riocli/package/deployment.py index 46790f26..398dc368 100644 --- a/riocli/package/deployment.py +++ b/riocli/package/deployment.py @@ -16,13 +16,16 @@ from riocli.config import new_v2_client from riocli.deployment.list import display_deployment_list from riocli.package.util import find_package -from riocli.utils.selector import show_selection -@click.command('deployments') -@click.option('--version', 'package_version', type=str, - help='Semantic version of the Package, only used when name is used instead of GUID') -@click.argument('package-name') +@click.command("deployments") +@click.option( + "--version", + "package_version", + type=str, + help="Semantic version of the Package, only used when name is used instead of GUID", +) +@click.argument("package-name") def list_package_deployments(package_name: str, package_version: str) -> None: """ List the deployments of the package @@ -32,13 +35,17 @@ def list_package_deployments(package_name: str, package_version: str) -> None: package_obj = find_package(client, package_name, package_version) if not package_obj: - click.secho("package not found", fg='red') + click.secho("package not found", fg="red") raise SystemExit(1) deployments = client.list_deployments( - query={'packageName': package_obj.metadata.name, 'packageVersion': package_obj.metadata.version}) + query={ + "packageName": package_obj.metadata.name, + "packageVersion": package_obj.metadata.version, + } + ) display_deployment_list(deployments, show_header=True) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) diff --git a/riocli/package/enum.py b/riocli/package/enum.py index c80e5016..669e253b 100644 --- a/riocli/package/enum.py +++ b/riocli/package/enum.py @@ -14,4 +14,4 @@ def __str__(self): Always = "always" Never = "no" - OnFailure = "on-failure" \ No newline at end of file + OnFailure = "on-failure" diff --git a/riocli/package/inspect.py b/riocli/package/inspect.py index 5fd7f91e..6493e85a 100644 --- a/riocli/package/inspect.py +++ b/riocli/package/inspect.py @@ -17,15 +17,23 @@ from riocli.config import new_v2_client from riocli.package.util import find_package from riocli.utils import inspect_with_format -from riocli.utils.selector import show_selection -@click.command('inspect') -@click.option('--version', 'package_version', type=str, - help='Semantic version of the Package, only used when name is used instead of GUID') -@click.option('--format', '-f', 'format_type', default='yaml', - type=click.Choice(['json', 'yaml'], case_sensitive=False)) -@click.argument('package-name') +@click.command("inspect") +@click.option( + "--version", + "package_version", + type=str, + help="Semantic version of the Package, only used when name is used instead of GUID", +) +@click.option( + "--format", + "-f", + "format_type", + default="yaml", + type=click.Choice(["json", "yaml"], case_sensitive=False), +) +@click.argument("package-name") def inspect_package(format_type: str, package_name: str, package_version: str) -> None: """ Inspect the package resource @@ -34,11 +42,11 @@ def inspect_package(format_type: str, package_name: str, package_version: str) - client = new_v2_client() package_obj = find_package(client, package_name, package_version) if not package_obj: - click.secho("package not found", fg='red') + click.secho("package not found", fg="red") raise SystemExit(1) inspect_with_format(unmunchify(package_obj), format_type) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) diff --git a/riocli/package/list.py b/riocli/package/list.py index 1d6b556c..2f88136e 100644 --- a/riocli/package/list.py +++ b/riocli/package/list.py @@ -20,9 +20,14 @@ from riocli.utils import tabulate_data -@click.command('list') -@click.option('--filter', 'filter_word', type=str, default=None, - help='A sub-string can be provided to filter down the package list') +@click.command("list") +@click.option( + "--filter", + "filter_word", + type=str, + default=None, + help="A sub-string can be provided to filter down the package list", +) def list_packages(filter_word: str) -> None: """ List the packages in the selected project @@ -32,19 +37,19 @@ def list_packages(filter_word: str) -> None: packages = client.list_packages() _display_package_list(packages, filter_word, show_header=True) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) def _display_package_list( - packages: typing.List[Package], - filter_word: str, - show_header: bool = True, - truncate_limit: int = 48, + packages: typing.List[Package], + filter_word: str, + show_header: bool = True, + truncate_limit: int = 48, ) -> None: headers = [] if show_header: - headers = ('Name', 'Version', 'Package ID', 'Description') + headers = ("Name", "Version", "Package ID", "Description") # Show IO Packages first iter_pkg = list(map(lambda x: x.metadata.name, packages)) @@ -57,7 +62,7 @@ def _display_package_list( package_dict[pkgName] = filtered_pkg data = [] - for pkgName, pkgVersionList in package_dict.items(): + for _, pkgVersionList in package_dict.items(): for package in pkgVersionList: description = package.metadata.get("description", "") name = package.metadata.name @@ -69,9 +74,11 @@ def _display_package_list( if truncate_limit: if len(description) > truncate_limit: - description = description[:truncate_limit] + '..' + description = description[:truncate_limit] + ".." if len(name) > truncate_limit: - name = name[:truncate_limit] + '..' + name = name[:truncate_limit] + ".." - data.append([name, package.metadata.version, package.metadata.guid, description]) + data.append( + [name, package.metadata.version, package.metadata.guid, description] + ) tabulate_data(data, headers=headers) diff --git a/riocli/package/model.py b/riocli/package/model.py index 8c7cb57f..abd22b8b 100644 --- a/riocli/package/model.py +++ b/riocli/package/model.py @@ -25,9 +25,9 @@ class Package(Model): RESTART_POLICY = { - 'always': RestartPolicy.Always, - 'never': RestartPolicy.Never, - 'onfailure': RestartPolicy.OnFailure + "always": RestartPolicy.Always, + "never": RestartPolicy.Never, + "onfailure": RestartPolicy.OnFailure, } def __init__(self, *args, **kwargs): @@ -50,8 +50,7 @@ def delete(self, *args, **kwargs) -> None: try: client.delete_package( - self.metadata.name, - query={"version": self.metadata.version} + self.metadata.name, query={"version": self.metadata.version} ) except HttpNotFoundError: raise ResourceNotFound @@ -68,13 +67,13 @@ def _sanitize_package(self) -> typing.Dict: def _sanitize_command(self): for e in self.spec.executables: # Skip if command is not set. - if e.get('command') is None: + if e.get("command") is None: continue c = [] - if e.get('runAsBash'): - c = ['/bin/bash', '-c'] + if e.get("runAsBash"): + c = ["/bin/bash", "-c"] if isinstance(e.command, list): c.extend(e.command) diff --git a/riocli/package/util.py b/riocli/package/util.py index 8f336d52..82e75ee2 100644 --- a/riocli/package/util.py +++ b/riocli/package/util.py @@ -23,57 +23,67 @@ from riocli.v2client import Client -def find_package(client: Client, - package_name: str, - package_version: str, - ) -> Munch: - +def find_package( + client: Client, + package_name: str, + package_version: str, +) -> Munch: package_obj = None if package_name.startswith("pkg-"): packages = client.list_packages(query={"guid": package_name}) if not packages: - raise Exception('Package not found') + raise Exception("Package not found") obj = packages[0] - package_obj = client.get_package(obj.metadata.name, query={"version": obj.metadata.version}) + package_obj = client.get_package( + obj.metadata.name, query={"version": obj.metadata.version} + ) elif package_name and package_version: package_obj = client.get_package(package_name, query={"version": package_version}) elif package_name: packages = client.list_packages(query={"name": package_name}) if len(packages) == 0: - click.secho("package not found", fg='red') + click.secho("package not found", fg="red") raise SystemExit(1) if len(packages) == 1: obj = packages[0] - package_obj = client.get_package(obj.metadata.name, query={"version": obj.metadata.version}) + package_obj = client.get_package( + obj.metadata.name, query={"version": obj.metadata.version} + ) else: options = {} package_objs = {} for pkg in packages: - options[pkg.metadata.guid] = '{} ({})'.format(pkg.metadata.name, pkg.metadata.version) + options[pkg.metadata.guid] = "{} ({})".format( + pkg.metadata.name, pkg.metadata.version + ) package_objs[pkg.metadata.guid] = pkg - choice = show_selection(options, header='Following packages were found with the same name') + choice = show_selection( + options, header="Following packages were found with the same name" + ) obj = package_objs[choice] - package_obj = client.get_package(obj.metadata.name, query={"version": obj.metadata.version}) + package_obj = client.get_package( + obj.metadata.name, query={"version": obj.metadata.version} + ) return package_obj def fetch_packages( - client: Client, - package_name_or_regex: str, - package_version: str, - include_all: bool, + client: Client, + package_name_or_regex: str, + package_version: str, + include_all: bool, ) -> List[Package]: packages = client.list_packages() result = [] for pkg in packages: # We cannot delete public packages. Skip them instead. - if 'io-public' in pkg.metadata.guid: + if "io-public" in pkg.metadata.guid: continue if include_all: @@ -90,6 +100,6 @@ def fetch_packages( def print_packages_for_confirmation(packages: List[Package]) -> None: - headers = ['Name', 'Version'] + headers = ["Name", "Version"] data = [[p.metadata.name, p.metadata.version] for p in packages] tabulate_data(data, headers) diff --git a/riocli/parameter/apply.py b/riocli/parameter/apply.py index 292ad308..e2db207b 100644 --- a/riocli/parameter/apply.py +++ b/riocli/parameter/apply.py @@ -30,29 +30,49 @@ @click.command( - 'apply', + "apply", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--devices', type=click.STRING, multiple=True, default=(), - help='Device names to apply configurations. If --device_name_pattern is' - 'provided, this will be ignored.') -@click.option('--device-name-pattern', type=click.STRING, multiple=False, - help='Device name regex pattern to apply configurations. Does not work with --devices.') -@click.option('--tree-names', type=click.STRING, multiple=True, default=None, - help='Tree names to apply to the device(s)') -@click.option('--retry-limit', type=click.INT, default=0, - help='Retry limit') -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, - type=click.BOOL, default=False, - help="Skip confirmation") +@click.option( + "--devices", + type=click.STRING, + multiple=True, + default=(), + help="Device names to apply configurations. If --device_name_pattern is" + "provided, this will be ignored.", +) +@click.option( + "--device-name-pattern", + type=click.STRING, + multiple=False, + help="Device name regex pattern to apply configurations. Does not work with --devices.", +) +@click.option( + "--tree-names", + type=click.STRING, + multiple=True, + default=None, + help="Tree names to apply to the device(s)", +) +@click.option("--retry-limit", type=click.INT, default=0, help="Retry limit") +@click.option( + "-f", + "--force", + "--silent", + "silent", + is_flag=True, + type=click.BOOL, + default=False, + help="Skip confirmation", +) def apply_configurations( - devices: typing.List, - tree_names: typing.List[str] = None, - retry_limit: int = 0, - device_name_pattern: str = None, - silent: bool = False, + devices: typing.List, + tree_names: typing.List[str] = None, + retry_limit: int = 0, + device_name_pattern: str = None, + silent: bool = False, ) -> None: """Apply a set of configuration parameter trees to a list of devices. @@ -87,8 +107,10 @@ def apply_configurations( # If device_name_pattern is specified, fetch devices based on the pattern # else fetch all devices. That means, include_all is False if # device_name_pattern is specified for the fetch_devices function. - include_all = not len(device_name_pattern or '') > 0 - online_devices = fetch_devices(client, device_name_pattern, include_all=include_all, online_devices=True) + include_all = not len(device_name_pattern or "") > 0 + online_devices = fetch_devices( + client, device_name_pattern, include_all=include_all, online_devices=True + ) device_map = {d.name: d for d in online_devices} @@ -96,54 +118,54 @@ def apply_configurations( # list of device names. But if device_name_pattern is specified # this list of devices will not be honoured. if devices and not device_name_pattern: - device_ids = {device_map[d].uuid: d for d in devices if - d in device_map} + device_ids = {device_map[d].uuid: d for d in devices if d in device_map} else: device_ids = {v.uuid: k for k, v in device_map.items()} if not device_ids: click.secho( "{} No device(s) found online. Please check the name or pattern".format( - Symbols.ERROR), - fg=Colors.RED) + Symbols.ERROR + ), + fg=Colors.RED, + ) raise SystemExit(1) - click.secho('Online Devices: {}'.format(','.join(device_ids.values())), - fg=Colors.GREEN) + click.secho( + "Online Devices: {}".format(",".join(device_ids.values())), fg=Colors.GREEN + ) - printable_tree_names = ','.join( - tree_names) if tree_names != "" else "*all*" + printable_tree_names = ",".join(tree_names) if tree_names != "" else "*all*" - click.secho('Config Trees: {}'.format(printable_tree_names), - fg=Colors.GREEN) + click.secho("Config Trees: {}".format(printable_tree_names), fg=Colors.GREEN) if not silent: click.confirm( - "Do you want to apply the configurations?", - default=True, abort=True) + "Do you want to apply the configurations?", default=True, abort=True + ) - with Spinner(text='Applying parameters...'): + with Spinner(text="Applying parameters..."): response = client.apply_parameters( - list(device_ids.keys()), - list(tree_names), - retry_limit + list(device_ids.keys()), list(tree_names), retry_limit ) print_separator() result = [] for device in response: - device_name = device_ids[device['device_id']] - success = device['success'] or "Partial" + device_name = device_ids[device["device_id"]] + success = device["success"] or "Partial" result.append([device_name, success]) tabulate_data(result, headers=["Device", "Success"]) except Exception as e: - click.secho('{} Failed to apply configs: {}'.format(Symbols.ERROR, e), fg=Colors.RED) + click.secho( + "{} Failed to apply configs: {}".format(Symbols.ERROR, e), fg=Colors.RED + ) raise SystemExit(1) from e def validate_trees(tree_names: typing.List[str]) -> None: available_trees = set(list_trees()) if not set(tree_names).issubset(available_trees): - raise Exception('one or more specified tree names are invalid') + raise Exception("one or more specified tree names are invalid") diff --git a/riocli/parameter/delete.py b/riocli/parameter/delete.py index 2d84ac91..71b1f61f 100644 --- a/riocli/parameter/delete.py +++ b/riocli/parameter/delete.py @@ -28,38 +28,41 @@ @click.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, - default=False, - help="Skip confirmation") -@click.argument('tree', type=click.STRING) -def delete_configurations( - tree: str, - silent: bool = False -) -> None: +@click.option( + "-f", + "--force", + "--silent", + "silent", + is_flag=True, + default=False, + help="Skip confirmation", +) +@click.argument("tree", type=click.STRING) +def delete_configurations(tree: str, silent: bool = False) -> None: """Delete a configuration parameter tree. You can skip the confirmation prompt by using the ``--force`` or ``--silent`` or ``-f`` flag. """ - click.secho('Configuration Parameter {} will be deleted'.format(tree)) + click.secho("Configuration Parameter {} will be deleted".format(tree)) if not silent: - click.confirm('Do you want to proceed?', default=True, abort=True) + click.confirm("Do you want to proceed?", default=True, abort=True) - with Spinner(text='Deleting...', timer=True) as spinner: + with Spinner(text="Deleting...", timer=True) as spinner: try: data = _api_call(HttpMethod.DELETE, name=tree) - if data.get('data') != 'ok': - raise Exception('Failed to delete configuration') + if data.get("data") != "ok": + raise Exception("Failed to delete configuration") spinner.text = click.style( - 'Configuration deleted successfully.', - fg=Colors.GREEN) + "Configuration deleted successfully.", fg=Colors.GREEN + ) spinner.green.ok(Symbols.SUCCESS) except IOError as e: spinner.text = click.style(e, fg=Colors.RED) diff --git a/riocli/parameter/diff.py b/riocli/parameter/diff.py index 9873aa86..6ae7ef02 100644 --- a/riocli/parameter/diff.py +++ b/riocli/parameter/diff.py @@ -33,14 +33,19 @@ @click.command( - 'diff', + "diff", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--tree-names', type=click.STRING, multiple=True, default=None, - help='Tree names to fetch') -@click.argument('path', type=click.Path(exists=True), required=False) +@click.option( + "--tree-names", + type=click.STRING, + multiple=True, + default=None, + help="Tree names to fetch", +) +@click.argument("path", type=click.Path(exists=True), required=False) def diff_configurations(path: str, tree_names: Tuple = None) -> None: """Diff between the local and cloud configuration trees. @@ -50,9 +55,8 @@ def diff_configurations(path: str, tree_names: Tuple = None) -> None: try: client = new_client() - with TemporaryDirectory(prefix='riocli-') as tmp_path: - client.download_configurations(tmp_path, - tree_names=list(tree_names)) + with TemporaryDirectory(prefix="riocli-") as tmp_path: + client.download_configurations(tmp_path, tree_names=list(tree_names)) for tree in trees: left_tree = os.path.join(tmp_path, tree) @@ -67,55 +71,66 @@ def diff_tree(left: str, right: str) -> None: comp = dircmp(left, right) for f in comp.common_dirs: - remote_dir, local_dir = os.path.join(comp.left, f), os.path.join( - comp.right, f) + remote_dir, local_dir = os.path.join(comp.left, f), os.path.join(comp.right, f) diff_tree(remote_dir, local_dir) for f in comp.diff_files: - remote_file, local_file = os.path.join(comp.left, f), os.path.join( - comp.right, f) + remote_file, local_file = ( + os.path.join(comp.left, f), + os.path.join(comp.right, f), + ) diff_file(remote_file, local_file) for f in comp.right_only: - remote_file, local_file = os.path.join(comp.left, f), os.path.join( - comp.right, f) + remote_file, local_file = ( + os.path.join(comp.left, f), + os.path.join(comp.right, f), + ) changed_file(remote_file, local_file, right_only=True) for f in comp.left_only: - remote_file, local_file = os.path.join(comp.left, f), os.path.join( - comp.right, f) + remote_file, local_file = ( + os.path.join(comp.left, f), + os.path.join(comp.right, f), + ) changed_file(remote_file, local_file, left_only=True) def diff_file(left: str, right: str): try: - with open(left, 'r', encoding='utf-8') as left_f: + with open(left, "r", encoding="utf-8") as left_f: left_lines = left_f.readlines() - with open(right, 'r', encoding='utf-8') as right_f: + with open(right, "r", encoding="utf-8") as right_f: right_lines = right_f.readlines() except UnicodeDecodeError: changed_file(left, right, binary=True) return - diff = unified_diff(left_lines, right_lines, fromfile=left, tofile=right, - lineterm='\n') + diff = unified_diff( + left_lines, right_lines, fromfile=left, tofile=right, lineterm="\n" + ) for line in diff: click.secho(line, nl=False) -def changed_file(left: str, right: str, left_only: bool = False, - right_only: bool = False, binary: bool = False): - click.secho('--- {}'.format(left)) - click.secho('+++ {}'.format(right)) +def changed_file( + left: str, + right: str, + left_only: bool = False, + right_only: bool = False, + binary: bool = False, +): + click.secho("--- {}".format(left)) + click.secho("+++ {}".format(right)) if left_only: - click.secho('deleted file') + click.secho("deleted file") click.secho() elif right_only: - click.secho('new file') + click.secho("new file") click.secho() elif binary: - click.secho('binary file changed') + click.secho("binary file changed") click.secho() diff --git a/riocli/parameter/download.py b/riocli/parameter/download.py index 5a9612c7..8a1bed8d 100644 --- a/riocli/parameter/download.py +++ b/riocli/parameter/download.py @@ -32,23 +32,32 @@ @click.command( - 'download', + "download", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--tree-names', type=click.STRING, multiple=True, default=None, - help='Tree names to fetch') -@click.option('--overwrite', '--delete-existing', 'delete_existing', - is_flag=True, - help='Overwrite existing parameter tree') -@click.argument('path', type=click.Path(exists=True), required=False) -@with_spinner(text='Download configurations...', timer=True) +@click.option( + "--tree-names", + type=click.STRING, + multiple=True, + default=None, + help="Tree names to fetch", +) +@click.option( + "--overwrite", + "--delete-existing", + "delete_existing", + is_flag=True, + help="Overwrite existing parameter tree", +) +@click.argument("path", type=click.Path(exists=True), required=False) +@with_spinner(text="Download configurations...", timer=True) def download_configurations( - path: str, - tree_names: typing.Tuple[str] = None, - delete_existing: bool = False, - spinner=None + path: str, + tree_names: typing.Tuple[str] = None, + delete_existing: bool = False, + spinner=None, ) -> None: """Download configuration parameter trees from rapyuta.io. @@ -65,23 +74,26 @@ def download_configurations( path = mkdtemp() if not tree_names: - msg = click.style('{} No tree names specified. Downloading all the trees...'.format(Symbols.INFO), - fg=Colors.BRIGHT_CYAN) + msg = click.style( + "{} No tree names specified. Downloading all the trees...".format( + Symbols.INFO + ), + fg=Colors.BRIGHT_CYAN, + ) spinner.write(msg) - spinner.write('Downloading at {}'.format(abspath(path))) + spinner.write("Downloading at {}".format(abspath(path))) try: client = new_client() client.download_configurations( - path, - tree_names=list(tree_names), - delete_existing_trees=delete_existing + path, tree_names=list(tree_names), delete_existing_trees=delete_existing ) - spinner.text = click.style("Configurations downloaded successfully.", - fg=Colors.GREEN) + spinner.text = click.style( + "Configurations downloaded successfully.", fg=Colors.GREEN + ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: spinner.text = click.style(e, fg=Colors.RED) diff --git a/riocli/parameter/list.py b/riocli/parameter/list.py index 8fde8655..1d4908f0 100644 --- a/riocli/parameter/list.py +++ b/riocli/parameter/list.py @@ -20,7 +20,7 @@ @click.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, @@ -30,7 +30,7 @@ def list_configuration_trees() -> None: try: data = list_trees() trees = [[tree] for tree in data] - tabulate_data(trees, headers=['Tree Name']) + tabulate_data(trees, headers=["Tree Name"]) except Exception as e: click.secho(str(e), fg=Colors.RED) raise SystemExit(1) diff --git a/riocli/parameter/upload.py b/riocli/parameter/upload.py index 4af7a3eb..f7850f39 100644 --- a/riocli/parameter/upload.py +++ b/riocli/parameter/upload.py @@ -28,24 +28,40 @@ @click.command( - 'upload', + "upload", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--tree-names', type=click.STRING, multiple=True, default=[], - help='Directory names to upload') -@click.option('--recreate', '--delete-existing', 'delete_existing', - is_flag=True, - help='Overwrite existing parameter tree') -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, - default=False, help="Skip confirmation") -@click.argument('path', type=click.Path(exists=True)) +@click.option( + "--tree-names", + type=click.STRING, + multiple=True, + default=[], + help="Directory names to upload", +) +@click.option( + "--recreate", + "--delete-existing", + "delete_existing", + is_flag=True, + help="Overwrite existing parameter tree", +) +@click.option( + "-f", + "--force", + "--silent", + "silent", + is_flag=True, + default=False, + help="Skip confirmation", +) +@click.argument("path", type=click.Path(exists=True)) def upload_configurations( - path: str, - tree_names: typing.Tuple[str] = None, - delete_existing: bool = False, - silent: bool = False + path: str, + tree_names: typing.Tuple[str] = None, + delete_existing: bool = False, + silent: bool = False, ) -> None: """Upload directories as configuration parameter trees. @@ -79,19 +95,22 @@ def upload_configurations( try: trees = filter_trees(path, tree_names) except Exception as e: - click.secho('{} {}'.format(Symbols.ERROR, e), fg=Colors.RED) + click.secho("{} {}".format(Symbols.ERROR, e), fg=Colors.RED) raise SystemExit(1) if not trees: - click.secho('{} No configuration trees to upload.'.format(Symbols.INFO), fg=Colors.BRIGHT_CYAN) + click.secho( + "{} No configuration trees to upload.".format(Symbols.INFO), + fg=Colors.BRIGHT_CYAN, + ) return - click.secho('Following configuration trees will be uploaded') + click.secho("Following configuration trees will be uploaded") click.secho() display_trees(path, trees) if not silent: - click.confirm('Do you want to proceed?', default=True, abort=True) + click.confirm("Do you want to proceed?", default=True, abort=True) client = new_client() @@ -101,12 +120,12 @@ def upload_configurations( rootdir=path, tree_names=trees, delete_existing_trees=delete_existing, - as_folder=True + as_folder=True, ) spinner.text = click.style( - 'Configuration parameters uploaded successfully', - fg=Colors.GREEN) + "Configuration parameters uploaded successfully", fg=Colors.GREEN + ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: spinner.text = click.style(e, fg=Colors.RED) diff --git a/riocli/parameter/utils.py b/riocli/parameter/utils.py index dd454f31..31bbaa33 100644 --- a/riocli/parameter/utils.py +++ b/riocli/parameter/utils.py @@ -28,10 +28,7 @@ from riocli.constants import Colors -def filter_trees( - root_dir: str, - tree_names: typing.Tuple[str] -) -> typing.List[str]: +def filter_trees(root_dir: str, tree_names: typing.Tuple[str]) -> typing.List[str]: trees = [] for each in os.listdir(root_dir): full_path = os.path.join(root_dir, each) @@ -43,12 +40,14 @@ def filter_trees( continue if not is_valid_tree_name(each): - raise Exception('Invalid tree name \'{}\'. Tree name must be 3-50 characters ' - 'and can contain letters, digits, _ and -'.format(each)) + raise Exception( + "Invalid tree name '{}'. Tree name must be 3-50 characters " + "and can contain letters, digits, _ and -".format(each) + ) trees.append(each) if tree_names and not trees: - raise Exception('one or more specified tree names are invalid') + raise Exception("one or more specified tree names are invalid") return trees @@ -61,52 +60,48 @@ def display_trees(root_dir: str, trees: typing.List[str]) -> None: def _api_call( - method: str, - name: typing.Union[str, None] = None, - payload: typing.Union[typing.Dict, None] = None, - load_response: bool = True, + method: str, + name: typing.Union[str, None] = None, + payload: typing.Union[typing.Dict, None] = None, + load_response: bool = True, ) -> typing.Any: config = Configuration() catalog_host = config.data.get( - 'core_api_host', 'https://gaapiserver.apps.okd4v2.prod.rapyuta.io') - url = '{}/api/paramserver/tree'.format(catalog_host) + "core_api_host", "https://gaapiserver.apps.okd4v2.prod.rapyuta.io" + ) + url = "{}/api/paramserver/tree".format(catalog_host) if name: - url = '{}/{}'.format(url, name) + url = "{}/{}".format(url, name) headers = config.get_auth_header() - response = RestClient(url).method(method).headers(headers).execute( - payload=payload) + response = RestClient(url).method(method).headers(headers).execute(payload=payload) data = None - err_msg = 'error in the api call' + err_msg = "error in the api call" if load_response: data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception(err_msg) return data def list_trees() -> List[str]: resp = _api_call(HttpMethod.GET) - if 'data' not in resp: - raise Exception('Failed to list configurations') + if "data" not in resp: + raise Exception("Failed to list configurations") - return resp.get('data') + return resp.get("data") class DeepDirCmp(dircmp): - def phase3(self) -> None: # shallow=False enables the behaviour of matching the File content. The # original dircmp Class only compares os.Stat between the files, and # gives no way to modify the behaviour. - f_comp = filecmp.cmpfiles(self.left, - self.right, - self.common_files, - shallow=False) + f_comp = filecmp.cmpfiles(self.left, self.right, self.common_files, shallow=False) self.same_files, self.diff_files, self.funny_files = f_comp def is_valid_tree_name(name: str) -> bool: """Validates a config tree name""" - return bool(re.match(r'^[0-9A-Za-z][0-9A-Za-z._-]{0,49}$', name)) + return bool(re.match(r"^[0-9A-Za-z][0-9A-Za-z._-]{0,49}$", name)) diff --git a/riocli/project/__init__.py b/riocli/project/__init__.py index a45cf8f9..40dd856c 100644 --- a/riocli/project/__init__.py +++ b/riocli/project/__init__.py @@ -27,8 +27,8 @@ @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color="yellow", + help_options_color="green", ) def project() -> None: """ diff --git a/riocli/project/create.py b/riocli/project/create.py index a52a8e62..114d990b 100644 --- a/riocli/project/create.py +++ b/riocli/project/create.py @@ -21,24 +21,27 @@ @click.command( - 'create', + "create", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('project-name', type=str) -@click.option('--organization', 'organization_name', - help='Pass organization name for which project needs to be created. Default will be current organization') +@click.argument("project-name", type=str) +@click.option( + "--organization", + "organization_name", + help="Pass organization name for which project needs to be created. Default will be current organization", +) @click.pass_context @name_to_organization_guid @with_spinner(text="Creating project...") def create_project( - ctx: click.Context, - project_name: str, - organization_guid: str, - organization_name: str, - organization_short_id: str, - spinner=None, + ctx: click.Context, + project_name: str, + organization_guid: str, + organization_name: str, + organization_short_id: str, + spinner=None, ) -> None: """Create a new project. @@ -46,27 +49,23 @@ def create_project( be created in the current organization. """ if not organization_guid: - organization_guid = ctx.obj.data.get('organization_id') + organization_guid = ctx.obj.data.get("organization_id") payload = { "metadata": { "name": project_name, "organizationGUID": organization_guid, - "labels": { - "createdBy": "rio-cli" - } + "labels": {"createdBy": "rio-cli"}, }, - "spec": {} + "spec": {}, } try: client = new_v2_client(with_project=False) client.create_project(payload) - spinner.text = click.style( - 'Project created successfully.', fg=Colors.GREEN) + spinner.text = click.style("Project created successfully.", fg=Colors.GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style( - 'Failed to create project: {}'.format(e), Colors.RED) + spinner.text = click.style("Failed to create project: {}".format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/project/delete.py b/riocli/project/delete.py index f4a182d1..31e9d1bf 100644 --- a/riocli/project/delete.py +++ b/riocli/project/delete.py @@ -21,21 +21,22 @@ @click.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--force', '-f', '--silent', 'force', is_flag=True, - help='Skip confirmation') -@click.argument('project-name', type=str) +@click.option( + "--force", "-f", "--silent", "force", is_flag=True, help="Skip confirmation" +) +@click.argument("project-name", type=str) @name_to_guid @with_spinner(text="Deleting project...") def delete_project( - force: bool, - project_name: str, - project_guid: str, - spinner=None, + force: bool, + project_name: str, + project_guid: str, + spinner=None, ) -> None: """Delete a project. @@ -44,17 +45,17 @@ def delete_project( """ if not force: with spinner.hidden(): - click.confirm('Deleting project {} ({})'.format( - project_name, project_guid), abort=True) + click.confirm( + "Deleting project {} ({})".format(project_name, project_guid), + abort=True, + ) try: client = new_v2_client() client.delete_project(project_guid) - spinner.text = click.style( - 'Project deleted successfully.', fg=Colors.GREEN) + spinner.text = click.style("Project deleted successfully.", fg=Colors.GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style( - 'Failed to delete project: {}'.format(e), Colors.RED) + spinner.text = click.style("Failed to delete project: {}".format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) diff --git a/riocli/project/features/vpn.py b/riocli/project/features/vpn.py index 939bc6b4..88923fe4 100644 --- a/riocli/project/features/vpn.py +++ b/riocli/project/features/vpn.py @@ -23,23 +23,28 @@ @click.command( - 'vpn', + "vpn", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('project-name', type=str) -@click.argument('enable', type=bool) -@click.option('--subnets', type=click.STRING, multiple=True, default=(), - help='Subnet ranges for the project. For example: 10.81.0.0/16') +@click.argument("project-name", type=str) +@click.argument("enable", type=bool) +@click.option( + "--subnets", + type=click.STRING, + multiple=True, + default=(), + help="Subnet ranges for the project. For example: 10.81.0.0/16", +) @name_to_guid @with_spinner(text="Updating VPN state...") def vpn( - project_name: str, - project_guid: str, - enable: bool, - subnets: List[str], - spinner=None, + project_name: str, + project_guid: str, + enable: bool, + subnets: List[str], + spinner=None, ) -> None: """ Enable or disable VPN on a project @@ -59,22 +64,19 @@ def vpn( spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e - project["spec"]["features"]["vpn"] = { - "enabled": enable, - "subnets": subnets or [] - } + project["spec"]["features"]["vpn"] = {"enabled": enable, "subnets": subnets or []} is_vpn_enabled = project["spec"]["features"]["vpn"].get("enabled", False) - status = 'Enabling VPN...' if enable else 'Disabling VPN...' + status = "Enabling VPN..." if enable else "Disabling VPN..." if is_vpn_enabled and subnets: - status = 'Updating the VPN subnet ranges for the project...' + status = "Updating the VPN subnet ranges for the project..." spinner.text = status try: client.update_project(project_guid, project) - spinner.text = click.style('Done', fg=Colors.GREEN) + spinner.text = click.style("Done", fg=Colors.GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: spinner.text = click.style("Failed: {}".format(e), fg=Colors.RED) diff --git a/riocli/project/inspect.py b/riocli/project/inspect.py index 341c8e1d..ba56bffe 100644 --- a/riocli/project/inspect.py +++ b/riocli/project/inspect.py @@ -22,17 +22,21 @@ @click.command( - 'inspect', + "inspect", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--format', '-f', 'format_type', default='yaml', - type=click.Choice(['json', 'yaml'], case_sensitive=False)) -@click.argument('project-name', type=str) +@click.option( + "--format", + "-f", + "format_type", + default="yaml", + type=click.Choice(["json", "yaml"], case_sensitive=False), +) +@click.argument("project-name", type=str) @name_to_guid -def inspect_project(format_type: str, project_name: str, - project_guid: str) -> None: +def inspect_project(format_type: str, project_name: str, project_guid: str) -> None: """Print the project details. You can specify the format of the output using the ``--format`` flag. diff --git a/riocli/project/list.py b/riocli/project/list.py index 7a0d1608..40fbf299 100644 --- a/riocli/project/list.py +++ b/riocli/project/list.py @@ -25,26 +25,35 @@ @click.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--organization', 'organization_name', - help='List projects for an organization') -@click.option('--label', '-l', 'labels', multiple=True, type=click.STRING, - default=(), help='Filter the deployment list by labels') -@click.option('--wide', '-w', is_flag=True, default=False, - help='Print more details', type=bool) +@click.option( + "--organization", "organization_name", help="List projects for an organization" +) +@click.option( + "--label", + "-l", + "labels", + multiple=True, + type=click.STRING, + default=(), + help="Filter the deployment list by labels", +) +@click.option( + "--wide", "-w", is_flag=True, default=False, help="Print more details", type=bool +) @click.pass_context @name_to_organization_guid def list_projects( - ctx: click.Context = None, - organization_guid: str = None, - organization_name: str = None, - organization_short_id: str = None, - labels: typing.List[str] = (), - wide: bool = False, + ctx: click.Context = None, + organization_guid: str = None, + organization_name: str = None, + organization_short_id: str = None, + labels: typing.List[str] = (), + wide: bool = False, ) -> None: """List all the projects you are a part of in current organization. @@ -64,34 +73,39 @@ def list_projects( $ rio project list --wide """ # If organization is not passed in the options, use - organization_guid = organization_guid or ctx.obj.data.get('organization_id') + organization_guid = organization_guid or ctx.obj.data.get("organization_id") if organization_guid is None: - err_msg = ('Organization not selected. Please set an organization with ' - '`rio organization select ORGANIZATION_NAME` or pass the --organization option.') - click.secho('{} {}'.format(Symbols.ERROR, err_msg), fg=Colors.RED) + err_msg = ( + "Organization not selected. Please set an organization with " + "`rio organization select ORGANIZATION_NAME` or pass the --organization option." + ) + click.secho("{} {}".format(Symbols.ERROR, err_msg), fg=Colors.RED) raise SystemExit(1) - query = {'labelSelector': labels} + query = {"labelSelector": labels} try: client = new_v2_client(with_project=False) projects = client.list_projects(organization_guid=organization_guid, query=query) projects = sorted(projects, key=lambda p: p.metadata.name.lower()) - current = ctx.obj.data.get('project_id', None) + current = ctx.obj.data.get("project_id", None) _display_project_list(projects, current, show_header=True, wide=wide) except Exception as e: click.secho(str(e), fg=Colors.RED) raise SystemExit(1) -def _display_project_list(projects: typing.List[munch.Munch], current: str = None, - show_header: bool = True, - wide: bool = False) -> None: +def _display_project_list( + projects: typing.List[munch.Munch], + current: str = None, + show_header: bool = True, + wide: bool = False, +) -> None: headers = [] if show_header: - headers = ['Project ID', 'Project Name', 'Status'] + headers = ["Project ID", "Project Name", "Status"] if wide: - headers.extend(['Created At', 'Creator', "Features"]) + headers.extend(["Created At", "Creator", "Features"]) data = [] for project in projects: @@ -102,8 +116,13 @@ def _display_project_list(projects: typing.List[munch.Munch], current: str = Non bold = True row = [metadata.guid, metadata.name, project.status.status] if wide: - row.extend([metadata.createdAt, metadata.creatorGUID, - unmunchify(project.spec.features)]) + row.extend( + [ + metadata.createdAt, + metadata.creatorGUID, + unmunchify(project.spec.features), + ] + ) data.append([click.style(v, fg=fg, bold=bold) for v in row]) tabulate_data(data, headers) diff --git a/riocli/project/model.py b/riocli/project/model.py index d8b41de5..575f1ae7 100644 --- a/riocli/project/model.py +++ b/riocli/project/model.py @@ -26,7 +26,6 @@ class Project(Model): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.update(*args, **kwargs) @@ -37,15 +36,20 @@ def apply(self, *args, **kwargs) -> ApplyResult: project = unmunchify(self) # set organizationGUID irrespective of it being present in the manifest - project['metadata']['organizationGUID'] = Configuration().organization_guid + project["metadata"]["organizationGUID"] = Configuration().organization_guid try: - r = client.create_project(project) - wait(self.is_ready, timeout_seconds=PROJECT_READY_TIMEOUT, - sleep_seconds=(1, 30, 2)) + client.create_project(project) + wait( + self.is_ready, + timeout_seconds=PROJECT_READY_TIMEOUT, + sleep_seconds=(1, 30, 2), + ) return ApplyResult.CREATED except HttpAlreadyExistsError: - guid = find_project_guid(client, self.metadata.name, Configuration().organization_guid) + guid = find_project_guid( + client, self.metadata.name, Configuration().organization_guid + ) client.update_project(guid, project) return ApplyResult.UPDATED except Exception as e: @@ -55,7 +59,9 @@ def delete(self, *args, **kwargs) -> None: client = new_v2_client() try: - guid = find_project_guid(client, self.metadata.name, Configuration().data['organization_id']) + guid = find_project_guid( + client, self.metadata.name, Configuration().data["organization_id"] + ) client.delete_project(guid) except (HttpNotFoundError, ProjectNotFound): raise ResourceNotFound @@ -63,4 +69,4 @@ def delete(self, *args, **kwargs) -> None: def is_ready(self) -> bool: client = new_v2_client() projects = client.list_projects(query={"name": self.metadata.name}) - return projects[0].status.status == 'Success' + return projects[0].status.status == "Success" diff --git a/riocli/project/select.py b/riocli/project/select.py index 82de2ab5..bf8255e2 100644 --- a/riocli/project/select.py +++ b/riocli/project/select.py @@ -21,18 +21,18 @@ @click.command( - 'select', + "select", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('project-name', type=str) +@click.argument("project-name", type=str) @name_to_guid @click.pass_context def select_project( - ctx: click.Context, - project_name: str, - project_guid: str, + ctx: click.Context, + project_name: str, + project_guid: str, ) -> None: """Switch to a different project in the current organization. @@ -41,18 +41,21 @@ def select_project( """ ctx = get_root_context(ctx) - ctx.obj.data['project_id'] = project_guid - ctx.obj.data['project_name'] = project_name + ctx.obj.data["project_id"] = project_guid + ctx.obj.data["project_name"] = project_name ctx.obj.save() try: cleanup_hosts_file() except Exception as e: - click.secho(f'{Symbols.WARNING} Failed to ' - f'clean up hosts file: {str(e)}', fg=Colors.YELLOW) + click.secho( + f"{Symbols.WARNING} Failed to " f"clean up hosts file: {str(e)}", + fg=Colors.YELLOW, + ) - click.secho('{} Project {} ({}) is selected!'.format( - Symbols.SUCCESS, - project_name, - project_guid), - fg=Colors.GREEN) + click.secho( + "{} Project {} ({}) is selected!".format( + Symbols.SUCCESS, project_name, project_guid + ), + fg=Colors.GREEN, + ) diff --git a/riocli/project/update_owner.py b/riocli/project/update_owner.py index c32af786..197ef2cf 100644 --- a/riocli/project/update_owner.py +++ b/riocli/project/update_owner.py @@ -23,20 +23,20 @@ @click.command( - 'update-owner', + "update-owner", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('project-name', type=str, required=True) -@click.option('--user-email', type=str, help="Email of the new owner") +@click.argument("project-name", type=str, required=True) +@click.option("--user-email", type=str, help="Email of the new owner") @name_to_guid @click.pass_context def update_owner( - ctx: click.Context, - project_name: str, - project_guid: str, - user_email: str, + ctx: click.Context, + project_name: str, + project_guid: str, + user_email: str, ) -> None: """ Update the owner of the project. @@ -56,7 +56,9 @@ def update_owner( try: project = client.get_project(project_guid) except Exception as e: - click.secho('{} Failed to fetch project: {}'.format(Symbols.ERROR, e), fg=Colors.RED) + click.secho( + "{} Failed to fetch project: {}".format(Symbols.ERROR, e), fg=Colors.RED + ) raise SystemExit(1) project_users = project.spec.users @@ -67,31 +69,42 @@ def update_owner( try: validate_email(user_email) except EmailNotValidError as e: - click.secho('{} {} is not a valid email address'.format(Symbols.ERROR, user_email), fg=Colors.RED) + click.secho( + "{} {} is not a valid email address".format(Symbols.ERROR, user_email), + fg=Colors.RED, + ) raise SystemExit(1) from e for u in project_users: - if u['emailID'] == user_email: - user_guid = u['userGUID'] + if u["emailID"] == user_email: + user_guid = u["userGUID"] break else: - ranger = {u['userGUID']: '{} {} ({})'.format(u['firstName'], u['lastName'], u['emailID']) - for u in project_users} + ranger = { + u["userGUID"]: "{} {} ({})".format( + u["firstName"], u["lastName"], u["emailID"] + ) + for u in project_users + } user_guid = show_selection( ranger, - header='Select a new project owner:', - prompt='Select', + header="Select a new project owner:", + prompt="Select", show_keys=False, highlight_item=project.metadata.creatorGUID, ) if user_guid is None: - click.secho('{} User not found in project'.format(Symbols.ERROR), fg=Colors.RED) + click.secho("{} User not found in project".format(Symbols.ERROR), fg=Colors.RED) raise SystemExit(1) try: client.update_project_owner(project_guid, user_guid) - click.secho('{} Owner updated successfully'.format(Symbols.SUCCESS), fg=Colors.GREEN) + click.secho( + "{} Owner updated successfully".format(Symbols.SUCCESS), fg=Colors.GREEN + ) except Exception as e: - click.secho('{} Failed to update owner: {}'.format(Symbols.ERROR, e), fg=Colors.RED) + click.secho( + "{} Failed to update owner: {}".format(Symbols.ERROR, e), fg=Colors.RED + ) raise SystemExit(1) diff --git a/riocli/project/util.py b/riocli/project/util.py index 469a1594..844b830d 100644 --- a/riocli/project/util.py +++ b/riocli/project/util.py @@ -27,17 +27,17 @@ def name_to_guid(f: typing.Callable) -> typing.Callable: @functools.wraps(f) def decorated(**kwargs: typing.Any): ctx = click.get_current_context() - name = kwargs.pop('project_name') + name = kwargs.pop("project_name") guid = None if name is None: - guid = ctx.obj.data.get('project_id') - name = ctx.obj.data.get('project_name') + guid = ctx.obj.data.get("project_id") + name = ctx.obj.data.get("project_name") if not name: raise LoggedOut - if name.startswith('project-'): + if name.startswith("project-"): guid = name name = None @@ -48,21 +48,20 @@ def decorated(**kwargs: typing.Any): if guid is None: try: - organization = ctx.obj.data.get('organization_id') + organization = ctx.obj.data.get("organization_id") guid = find_project_guid(client, name, organization) except Exception as e: - click.secho('{} {}'.format(Symbols.ERROR, e), fg=Colors.RED) + click.secho("{} {}".format(Symbols.ERROR, e), fg=Colors.RED) raise SystemExit(1) - kwargs['project_name'] = name - kwargs['project_guid'] = guid + kwargs["project_name"] = name + kwargs["project_guid"] = guid f(**kwargs) return decorated -def find_project_guid(client: v2Client, name: str, - organization: str = None) -> str: +def find_project_guid(client: v2Client, name: str, organization: str = None) -> str: projects = client.list_projects(query={"name": name}, organization_guid=organization) if projects and projects[0].metadata.name == name: @@ -83,8 +82,7 @@ def find_organization_guid(client: Client, name: str) -> typing.Tuple[str, str]: if organization.name == name: return organization.guid, organization.short_guid - raise OrganizationNotFound( - "User is not part of organization: {}".format(name)) + raise OrganizationNotFound("User is not part of organization: {}".format(name)) def get_organization_name(client: Client, guid: str) -> typing.Tuple[str, str]: @@ -93,21 +91,20 @@ def get_organization_name(client: Client, guid: str) -> typing.Tuple[str, str]: if organization.guid == guid: return organization.name, organization.short_guid - raise OrganizationNotFound( - "User is not part of organization: {}".format(guid)) + raise OrganizationNotFound("User is not part of organization: {}".format(guid)) def name_to_organization_guid(f: typing.Callable) -> typing.Callable: @functools.wraps(f) def decorated(*args: typing.Any, **kwargs: typing.Any): client = new_client(with_project=False) - name = kwargs.get('organization_name') + name = kwargs.get("organization_name") guid = None short_id = None if name: try: - if name.startswith('org-'): + if name.startswith("org-"): guid = name name, short_id = get_organization_name(client, guid) else: @@ -116,9 +113,9 @@ def decorated(*args: typing.Any, **kwargs: typing.Any): click.secho(str(e), fg=Colors.RED) raise SystemExit(1) - kwargs['organization_name'] = name - kwargs['organization_guid'] = guid - kwargs['organization_short_id'] = short_id + kwargs["organization_name"] = name + kwargs["organization_guid"] = guid + kwargs["organization_short_id"] = short_id f(*args, **kwargs) @@ -126,12 +123,12 @@ def decorated(*args: typing.Any, **kwargs: typing.Any): class ProjectNotFound(Exception): - def __init__(self, message='project not found'): + def __init__(self, message="project not found"): self.message = message super().__init__(self.message) class OrganizationNotFound(Exception): - def __init__(self, message='organization not found'): + def __init__(self, message="organization not found"): self.message = message super().__init__(self.message) diff --git a/riocli/project/whoami.py b/riocli/project/whoami.py index 6fea259a..d6518b04 100644 --- a/riocli/project/whoami.py +++ b/riocli/project/whoami.py @@ -19,16 +19,16 @@ from riocli.exceptions import LoggedOut from riocli.project.util import name_to_guid -ADMIN_ROLE = 'admin' +ADMIN_ROLE = "admin" @click.command( - 'whoami', + "whoami", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('-p', '--project-name', type=str) +@click.option("-p", "--project-name", type=str) @name_to_guid @click.pass_context def whoami(ctx: click.Context, project_name: str, project_guid: str) -> None: @@ -37,7 +37,7 @@ def whoami(ctx: click.Context, project_name: str, project_guid: str) -> None: If you do not specify the project name, the command will use the project set in the CLI context. """ - if not ctx.obj.data.get('email_id'): + if not ctx.obj.data.get("email_id"): raise LoggedOut try: @@ -49,8 +49,8 @@ def whoami(ctx: click.Context, project_name: str, project_guid: str) -> None: def find_role( - config: Configuration, - project_guid: str = None, + config: Configuration, + project_guid: str = None, ) -> str: """ Find the role of the user in the project. @@ -65,16 +65,16 @@ def find_role( v2_client = config.new_v2_client() # The user email comes from the config - user_email = config.data.get('email_id') + user_email = config.data.get("email_id") if not user_email: - raise ValueError('User email cannot be found in the config.') + raise ValueError("User email cannot be found in the config.") # Default to the config values if not provided if not project_guid: - project_guid = config.data.get('project_id') + project_guid = config.data.get("project_id") if not project_guid: - raise ValueError('Project GUID cannot be found.') + raise ValueError("Project GUID cannot be found.") try: project = v2_client.get_project(project_guid) @@ -84,9 +84,9 @@ def find_role( role = None # If user is present in the users list, check if they are and admin - for user in project.spec.get('users', []): - if user['emailID'] == user_email: - role = user['role'] + for user in project.spec.get("users", []): + if user["emailID"] == user_email: + role = user["role"] break if role and role == ADMIN_ROLE: @@ -96,23 +96,23 @@ def find_role( # has access to and compare them with the list of groups where the project # is included. try: - user_groups = v1_client.list_usergroups(project.metadata.get('organizationGUID')) + user_groups = v1_client.list_usergroups(project.metadata.get("organizationGUID")) user_groups = {g.name: True for g in user_groups} except Exception as e: raise e - for group in project.spec.get('userGroups', []): - if group['name'] not in user_groups: + for group in project.spec.get("userGroups", []): + if group["name"] not in user_groups: continue # If the user is part of a group that has admin access then no # need to check further. - if role != ADMIN_ROLE and group['role'] == ADMIN_ROLE: + if role != ADMIN_ROLE and group["role"] == ADMIN_ROLE: return ADMIN_ROLE - role = group['role'] + role = group["role"] if not role: - raise Exception('User does not have access to the project') + raise Exception("User does not have access to the project") return role diff --git a/riocli/rosbag/__init__.py b/riocli/rosbag/__init__.py index d9e4dabe..fcdbabf9 100644 --- a/riocli/rosbag/__init__.py +++ b/riocli/rosbag/__init__.py @@ -21,8 +21,8 @@ @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color="yellow", + help_options_color="green", ) def rosbag() -> None: """ diff --git a/riocli/rosbag/blob.py b/riocli/rosbag/blob.py index dd27cc43..5f1ef632 100644 --- a/riocli/rosbag/blob.py +++ b/riocli/rosbag/blob.py @@ -24,11 +24,11 @@ @click.group( - 'blob', + "blob", invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color="yellow", + help_options_color="green", ) def rosbag_blob() -> None: """ @@ -37,8 +37,8 @@ def rosbag_blob() -> None: pass -@rosbag_blob.command('delete') -@click.argument('guid') +@rosbag_blob.command("delete") +@click.argument("guid") def blob_delete(guid: str) -> None: """ Delete a ROSbag blob @@ -47,16 +47,16 @@ def blob_delete(guid: str) -> None: client = new_client() with spinner(): client.delete_rosbag_blob(guid) - click.secho('Rosbag Blob deleted successfully', fg='green') + click.secho("Rosbag Blob deleted successfully", fg="green") except ResourceNotFoundError as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) -@rosbag_blob.command('download') -@click.argument('guid') -@click.option('--filename', help='Name of the file') -@click.option('--download-dir', help='Directory to download the file into') +@rosbag_blob.command("download") +@click.argument("guid") +@click.option("--filename", help="Name of the file") +@click.option("--download-dir", help="Directory to download the file into") def blob_download(guid: str, filename: str, download_dir: str) -> None: """ Download a ROSbag blob @@ -64,24 +64,40 @@ def blob_download(guid: str, filename: str, download_dir: str) -> None: try: client = new_client() with spinner(): - client.download_rosbag_blob(guid=guid, filename=filename, download_dir=download_dir) - click.secho('Rosbag Blob downloaded successfully', fg='green') + client.download_rosbag_blob( + guid=guid, filename=filename, download_dir=download_dir + ) + click.secho("Rosbag Blob downloaded successfully", fg="green") except ResourceNotFoundError as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) -@rosbag_blob.command('list') -@click.option('--guids', help='Filter by blob guids ', multiple=True) -@click.option('--deployment-ids', help='Filter by deployment ids ', multiple=True) -@click.option('--component-instance-ids', help='Filter by component instance ids ', multiple=True) -@click.option('--job-ids', help='Filter by job ids ', multiple=True) -@click.option('--device-ids', help='Filter by device ids ', multiple=True) -@click.option('--statuses', help='Filter by rosbag blob statuses ', - type=click.Choice(['Starting', 'Uploading', 'Uploaded', 'Error'], case_sensitive=True), - multiple=True, default=['Uploaded', 'Uploading', 'Starting']) -def blob_list(guids: typing.List[str], deployment_ids: typing.List[str], component_instance_ids: typing.List[str], - job_ids: typing.List[str], device_ids: typing.List[str], statuses: typing.List[str]) -> None: +@rosbag_blob.command("list") +@click.option("--guids", help="Filter by blob guids ", multiple=True) +@click.option("--deployment-ids", help="Filter by deployment ids ", multiple=True) +@click.option( + "--component-instance-ids", help="Filter by component instance ids ", multiple=True +) +@click.option("--job-ids", help="Filter by job ids ", multiple=True) +@click.option("--device-ids", help="Filter by device ids ", multiple=True) +@click.option( + "--statuses", + help="Filter by rosbag blob statuses ", + type=click.Choice( + ["Starting", "Uploading", "Uploaded", "Error"], case_sensitive=True + ), + multiple=True, + default=["Uploaded", "Uploading", "Starting"], +) +def blob_list( + guids: typing.List[str], + deployment_ids: typing.List[str], + component_instance_ids: typing.List[str], + job_ids: typing.List[str], + device_ids: typing.List[str], + statuses: typing.List[str], +) -> None: """ List the Rosbag blobs in the selected project """ @@ -90,30 +106,45 @@ def blob_list(guids: typing.List[str], deployment_ids: typing.List[str], compone status_list.append(ROSBagBlobStatus(status)) try: client = new_client() - rosbag_blobs = client.list_rosbag_blobs(guids=list(guids), deployment_ids=list(deployment_ids), - component_instance_ids=list(component_instance_ids), - job_ids=list(job_ids), - device_ids=list(device_ids), statuses=status_list) + rosbag_blobs = client.list_rosbag_blobs( + guids=list(guids), + deployment_ids=list(deployment_ids), + component_instance_ids=list(component_instance_ids), + job_ids=list(job_ids), + device_ids=list(device_ids), + statuses=status_list, + ) _display_rosbag_blob_list(rosbag_blobs, show_header=True) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) -def _display_rosbag_blob_list(blobs: typing.List[ROSBagBlob], show_header: bool = True) -> None: +def _display_rosbag_blob_list( + blobs: typing.List[ROSBagBlob], show_header: bool = True +) -> None: headers = [] if show_header: - header = ('GUID', 'Job ID', 'Blob Ref ID', 'Status', 'Component Type', 'Device ID') + headers = ( + "GUID", + "Job ID", + "Blob Ref ID", + "Status", + "Component Type", + "Device ID", + ) data = [] for blob in blobs: - data.append([ - blob.guid, - blob.job_id, - blob.blob_ref_id, - blob.status, - blob.component_type.name, - 'None' if blob.device_id is None else blob.device_id - ]) + data.append( + [ + blob.guid, + blob.job_id, + blob.blob_ref_id, + blob.status, + blob.component_type.name, + "None" if blob.device_id is None else blob.device_id, + ] + ) tabulate_data(data, headers) diff --git a/riocli/rosbag/job.py b/riocli/rosbag/job.py index 6bdfcf38..20581e43 100644 --- a/riocli/rosbag/job.py +++ b/riocli/rosbag/job.py @@ -17,10 +17,18 @@ import pyrfc3339 from click_help_colors import HelpColorsGroup from click_spinner import spinner -from rapyuta_io.clients.rosbag import ROSBagOptions, ROSBagJob, ROSBagCompression, ROSBagJobStatus, ROSBagUploadTypes, \ - ROSBagOnDemandUploadOptions, ROSBagTimeRange +from rapyuta_io.clients.rosbag import ( + ROSBagOptions, + ROSBagJob, + ROSBagCompression, + ROSBagJobStatus, + ROSBagUploadTypes, + ROSBagOnDemandUploadOptions, + ROSBagTimeRange, +) from riocli.config import new_client + # from riocli.deployment.util import name_to_guid as deployment_name_to_guid from riocli.rosbag.util import ROSBagJobNotFound from riocli.utils import inspect_with_format @@ -28,11 +36,11 @@ @click.group( - 'job', + "job", invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color="yellow", + help_options_color="green", ) def rosbag_job() -> None: """ @@ -41,50 +49,95 @@ def rosbag_job() -> None: pass -@rosbag_job.command('create') -@click.option('--name', help='Name of the rosbag job') -@click.option('--deployment-id', help='Deployment id ') -@click.option('--component-instance-id', help='Component instance id ') -@click.option('--all-topics/--not-all-topics', help='Record all topics?', default=False) -@click.option('--topics', help='List of topics whose content is to be recorded ', multiple=True) -@click.option('--topic-include-regex', help='Include topics matching the given regular expression ', multiple=True) -@click.option('--topic-exclude-regex', help='Exclude topics matching the given regular expression ') -@click.option('--max-message-count', help='Only record NUM messages on each topic ', default=0) -@click.option('--node', help='Record all topics subscribed to by a specific node ') -@click.option('--compression', help='Compression ?', type=click.Choice(['LZ4', 'BZ2'], case_sensitive=True), - default=None) -@click.option('--max-splits', help='Split bag at most MAX_SPLITS times ', default=0) -@click.option('--max-split-size', help='Record a bag of maximum size', default=0) -@click.option('--chunk-size', help='Record to chunks of size KB before writing to disk', default=0) -def job_create(name: str, deployment_id: str, component_instance_id: str, all_topics: bool, topics: typing.List[str], - topic_include_regex: str, topic_exclude_regex: str, max_message_count: int, node: str, - compression: str, max_splits: int, max_split_size: int, chunk_size: int) -> None: +@rosbag_job.command("create") +@click.option("--name", help="Name of the rosbag job") +@click.option("--deployment-id", help="Deployment id ") +@click.option("--component-instance-id", help="Component instance id ") +@click.option("--all-topics/--not-all-topics", help="Record all topics?", default=False) +@click.option( + "--topics", help="List of topics whose content is to be recorded ", multiple=True +) +@click.option( + "--topic-include-regex", + help="Include topics matching the given regular expression ", + multiple=True, +) +@click.option( + "--topic-exclude-regex", + help="Exclude topics matching the given regular expression ", +) +@click.option( + "--max-message-count", help="Only record NUM messages on each topic ", default=0 +) +@click.option("--node", help="Record all topics subscribed to by a specific node ") +@click.option( + "--compression", + help="Compression ?", + type=click.Choice(["LZ4", "BZ2"], case_sensitive=True), + default=None, +) +@click.option("--max-splits", help="Split bag at most MAX_SPLITS times ", default=0) +@click.option("--max-split-size", help="Record a bag of maximum size", default=0) +@click.option( + "--chunk-size", help="Record to chunks of size KB before writing to disk", default=0 +) +def job_create( + name: str, + deployment_id: str, + component_instance_id: str, + all_topics: bool, + topics: typing.List[str], + topic_include_regex: str, + topic_exclude_regex: str, + max_message_count: int, + node: str, + compression: str, + max_splits: int, + max_split_size: int, + chunk_size: int, +) -> None: """ Create a ROSbag job """ if compression: compression = ROSBagCompression(compression) - rosbag_options = ROSBagOptions(all_topics=all_topics, topics=list(topics), - topic_include_regex=list(topic_include_regex), - topic_exclude_regex=topic_exclude_regex, max_message_count=max_message_count, - node=node, max_splits=max_splits, max_split_size=max_split_size, - chunk_size=chunk_size, compression=compression) - rosbag_job = ROSBagJob(name=name, deployment_id=deployment_id, component_instance_id=component_instance_id, - rosbag_options=rosbag_options) + rosbag_options = ROSBagOptions( + all_topics=all_topics, + topics=list(topics), + topic_include_regex=list(topic_include_regex), + topic_exclude_regex=topic_exclude_regex, + max_message_count=max_message_count, + node=node, + max_splits=max_splits, + max_split_size=max_split_size, + chunk_size=chunk_size, + compression=compression, + ) + rosbag_job = ROSBagJob( + name=name, + deployment_id=deployment_id, + component_instance_id=component_instance_id, + rosbag_options=rosbag_options, + ) try: client = new_client() with spinner(): client.create_rosbag_job(rosbag_job) - click.secho('Rosbag Job created successfully', fg='green') + click.secho("Rosbag Job created successfully", fg="green") except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) -@rosbag_job.command('inspect') -@click.option('--format', '-f', 'format_type', - type=click.Choice(['json', 'yaml'], case_sensitive=False), default='yaml') -@click.argument('job-guid', type=str) +@rosbag_job.command("inspect") +@click.option( + "--format", + "-f", + "format_type", + type=click.Choice(["json", "yaml"], case_sensitive=False), + default="yaml", +) +@click.argument("job-guid", type=str) def job_inspect(job_guid: str, format_type: str) -> None: """ Inspect a ROSbag job @@ -95,41 +148,63 @@ def job_inspect(job_guid: str, format_type: str) -> None: job = make_rosbag_job_inspectable(job) inspect_with_format(job, format_type) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) -@rosbag_job.command('stop') -@click.argument('deployment-name') -@click.option('--component-instance-ids', help='Filter by component instance ids ', multiple=True) -@click.option('--guids', help='Filter by job guids', multiple=True) +@rosbag_job.command("stop") +@click.argument("deployment-name") +@click.option( + "--component-instance-ids", help="Filter by component instance ids ", multiple=True +) +@click.option("--guids", help="Filter by job guids", multiple=True) # @deployment_name_to_guid -def job_stop(deployment_guid: str, deployment_name: str, component_instance_ids: typing.List[str], - guids: typing.List[str]) -> None: +def job_stop( + deployment_guid: str, + deployment_name: str, + component_instance_ids: typing.List[str], + guids: typing.List[str], +) -> None: """ Stop ROSbag jobs """ try: client = new_client() with spinner(): - client.stop_rosbag_jobs(deployment_id=deployment_guid, component_instance_ids=list(component_instance_ids), - guids=list(guids)) - click.secho('Rosbag Job stopped successfully', fg='green') + client.stop_rosbag_jobs( + deployment_id=deployment_guid, + component_instance_ids=list(component_instance_ids), + guids=list(guids), + ) + click.secho("Rosbag Job stopped successfully", fg="green") except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) -@rosbag_job.command('list') -@click.argument('deployment-name') -@click.option('--component-instance-ids', help='Filter by component instance ids ', multiple=True) -@click.option('--guids', help='Filter by job guids ', multiple=True) -@click.option('--statuses', help='Filter by rosbag job statuses ', multiple=True, - default=['Starting', 'Running', 'Stopped', 'Stopping', 'Error'], - type=click.Choice(['Starting', 'Running', 'Error', 'Stopping', 'Stopped'], case_sensitive=True)) +@rosbag_job.command("list") +@click.argument("deployment-name") +@click.option( + "--component-instance-ids", help="Filter by component instance ids ", multiple=True +) +@click.option("--guids", help="Filter by job guids ", multiple=True) +@click.option( + "--statuses", + help="Filter by rosbag job statuses ", + multiple=True, + default=["Starting", "Running", "Stopped", "Stopping", "Error"], + type=click.Choice( + ["Starting", "Running", "Error", "Stopping", "Stopped"], case_sensitive=True + ), +) # @deployment_name_to_guid -def job_list(deployment_guid: str, deployment_name: str, - component_instance_ids: typing.List[str], guids: typing.List[str], statuses: typing.List[str]) -> None: +def job_list( + deployment_guid: str, + deployment_name: str, + component_instance_ids: typing.List[str], + guids: typing.List[str], + statuses: typing.List[str], +) -> None: """ List the Rosbag jobs for the given Deployment """ @@ -138,25 +213,41 @@ def job_list(deployment_guid: str, deployment_name: str, client = new_client() for status in list(statuses): status_list.append(ROSBagJobStatus(status)) - rosbag_jobs = client.list_rosbag_jobs(deployment_id=deployment_guid, - component_instance_ids=list(component_instance_ids), guids=list(guids), - statuses=status_list) + rosbag_jobs = client.list_rosbag_jobs( + deployment_id=deployment_guid, + component_instance_ids=list(component_instance_ids), + guids=list(guids), + statuses=status_list, + ) _display_rosbag_job_list(rosbag_jobs, show_header=True) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) -@rosbag_job.command('trigger') -@click.argument('deployment-name') -@click.argument('job-guid') -@click.option('--upload-from', help='Rosbags recorded after or at this time are uploaded. Specify time in RFC 3339 ' - 'format (1985-04-12T23:20:50.52Z)', required=True) -@click.option('--upload-to', help='Rosbags recorded before or at this time are uploaded. Specify time in RFC 3339 ' - 'format (1985-04-12T23:20:50.52Z)', required=True) +@rosbag_job.command("trigger") +@click.argument("deployment-name") +@click.argument("job-guid") +@click.option( + "--upload-from", + help="Rosbags recorded after or at this time are uploaded. Specify time in RFC 3339 " + "format (1985-04-12T23:20:50.52Z)", + required=True, +) +@click.option( + "--upload-to", + help="Rosbags recorded before or at this time are uploaded. Specify time in RFC 3339 " + "format (1985-04-12T23:20:50.52Z)", + required=True, +) # @deployment_name_to_guid -def job_trigger_upload(deployment_guid: str, deployment_name: str, job_guid: str, - upload_from: str, upload_to: str) -> None: +def job_trigger_upload( + deployment_guid: str, + deployment_name: str, + job_guid: str, + upload_from: str, + upload_to: str, +) -> None: """ Trigger Rosbag Upload @@ -195,70 +286,88 @@ def job_trigger_upload(deployment_guid: str, deployment_name: str, job_guid: str try: client = new_client() with spinner(): - rosbag_jobs = client.list_rosbag_jobs(deployment_id=deployment_guid, guids=[job_guid]) + rosbag_jobs = client.list_rosbag_jobs( + deployment_id=deployment_guid, guids=[job_guid] + ) if len(rosbag_jobs) == 0: raise ROSBagJobNotFound() - if rosbag_jobs[0].upload_options and \ - rosbag_jobs[0].upload_options.upload_type != ROSBagUploadTypes.ON_DEMAND: + if ( + rosbag_jobs[0].upload_options + and rosbag_jobs[0].upload_options.upload_type + != ROSBagUploadTypes.ON_DEMAND + ): click.secho( "Warning: this job does not have OnDemand upload type so triggering will not have any effect but," - " it will take into effect when job's upload type is changed to OnDemand", fg='yellow' + " it will take into effect when job's upload type is changed to OnDemand", + fg="yellow", ) time_range = ROSBagTimeRange( from_time=int(pyrfc3339.parse(upload_from).timestamp()), - to_time=int(pyrfc3339.parse(upload_to).timestamp()) + to_time=int(pyrfc3339.parse(upload_to).timestamp()), ) on_demand_options = ROSBagOnDemandUploadOptions(time_range) rosbag_jobs[0].patch(on_demand_options=on_demand_options) - click.secho('Rosbag upload triggered successfully', fg='green') + click.secho("Rosbag upload triggered successfully", fg="green") except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) -@rosbag_job.command('update') -@click.argument('deployment-name') -@click.argument('job-guid') -@click.option('--upload-mode', help='Change upload mode', - type=click.Choice([t for t in ROSBagUploadTypes]), required=True) +@rosbag_job.command("update") +@click.argument("deployment-name") +@click.argument("job-guid") +@click.option( + "--upload-mode", + help="Change upload mode", + type=click.Choice([t for t in ROSBagUploadTypes]), + required=True, +) # @deployment_name_to_guid -def update_job(deployment_guid: str, deployment_name: str, job_guid: str, upload_mode: str) -> None: +def update_job( + deployment_guid: str, deployment_name: str, job_guid: str, upload_mode: str +) -> None: """ Update the Rosbag Job """ try: client = new_client() with spinner(): - rosbag_jobs = client.list_rosbag_jobs(deployment_id=deployment_guid, guids=[job_guid]) + rosbag_jobs = client.list_rosbag_jobs( + deployment_id=deployment_guid, guids=[job_guid] + ) if len(rosbag_jobs) == 0: raise ROSBagJobNotFound() rosbag_jobs[0].patch(upload_type=upload_mode) - click.secho('Rosbag Job updated successfully', fg='green') + click.secho("Rosbag Job updated successfully", fg="green") except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") raise SystemExit(1) -def _display_rosbag_job_list(jobs: typing.List[ROSBagJob], show_header: bool = True) -> None: +def _display_rosbag_job_list( + jobs: typing.List[ROSBagJob], show_header: bool = True +) -> None: headers = [] if show_header: - headers = ('GUID', 'Name', 'Status', 'Component Type', 'Device ID') + headers = ("GUID", "Name", "Status", "Component Type", "Device ID") data = [] for job in jobs: - data.append([ - job.guid, - job.name, - job.status, - job.component_type.name, - 'None' if job.device_id is None else job.device_id, - ]) + data.append( + [ + job.guid, + job.name, + job.status, + job.component_type.name, + "None" if job.device_id is None else job.device_id, + ] + ) tabulate_data(data, headers) diff --git a/riocli/rosbag/util.py b/riocli/rosbag/util.py index 6b1ad35f..7d674b26 100644 --- a/riocli/rosbag/util.py +++ b/riocli/rosbag/util.py @@ -1,4 +1,4 @@ class ROSBagJobNotFound(Exception): - def __init__(self, message='rosbag job not found'): + def __init__(self, message="rosbag job not found"): self.message = message super().__init__(self.message) diff --git a/riocli/secret/delete.py b/riocli/secret/delete.py index 777d97ba..fa09b9d2 100644 --- a/riocli/secret/delete.py +++ b/riocli/secret/delete.py @@ -28,26 +28,38 @@ @click.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--force', '-f', '--silent', is_flag=True, default=False, - help='Skip confirmation') -@click.option('-a', '--all', 'delete_all', is_flag=True, default=False, - help='Deletes all secrets in the project') -@click.option('--workers', '-w', - help="Number of parallel workers while running delete secret " - "command. Defaults to 10.", type=int, default=10) -@click.argument('secret-name-or-regex', type=str, default="") +@click.option( + "--force", "-f", "--silent", is_flag=True, default=False, help="Skip confirmation" +) +@click.option( + "-a", + "--all", + "delete_all", + is_flag=True, + default=False, + help="Deletes all secrets in the project", +) +@click.option( + "--workers", + "-w", + help="Number of parallel workers while running delete secret " + "command. Defaults to 10.", + type=int, + default=10, +) +@click.argument("secret-name-or-regex", type=str, default="") @with_spinner(text="Deleting secret...") def delete_secret( - secret_name_or_regex: str, - force: bool, - delete_all: bool = False, - workers: int = 10, - spinner=None, + secret_name_or_regex: str, + force: bool, + delete_all: bool = False, + workers: int = 10, + spinner=None, ) -> None: """Delete one or more secrets with a name or a regex pattern. @@ -87,8 +99,7 @@ def delete_secret( try: routes = fetch_secrets(client, secret_name_or_regex, delete_all) except Exception as e: - spinner.text = click.style( - 'Failed to delete secret(s): {}'.format(e), Colors.RED) + spinner.text = click.style("Failed to delete secret(s): {}".format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -100,34 +111,35 @@ def delete_secret( with spinner.hidden(): print_secrets_for_confirmation(routes) - spinner.write('') + spinner.write("") if not force: with spinner.hidden(): - click.confirm('Do you want to delete the above secret(s)?', abort=True) - spinner.write('') + click.confirm("Do you want to delete the above secret(s)?", abort=True) + spinner.write("") try: f = functools.partial(_apply_delete, client) - result = apply_func_with_result(f=f, items=routes, workers=workers, key=lambda x: x[0]) + result = apply_func_with_result( + f=f, items=routes, workers=workers, key=lambda x: x[0] + ) data, statuses = [], [] for name, status, msg in result: fg = Colors.GREEN if status else Colors.RED icon = Symbols.SUCCESS if status else Symbols.ERROR statuses.append(status) - data.append([ - click.style(name, fg), - click.style('{} {}'.format(icon, msg), fg) - ]) + data.append( + [click.style(name, fg), click.style("{} {}".format(icon, msg), fg)] + ) with spinner.hidden(): - tabulate_data(data, headers=['Name', 'Status']) + tabulate_data(data, headers=["Name", "Status"]) # When no route is deleted, raise an exception. if not any(statuses): - spinner.write('') - spinner.text = click.style('Failed to delete secret(s).', Colors.RED) + spinner.write("") + spinner.text = click.style("Failed to delete secret(s).", Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) @@ -135,13 +147,11 @@ def delete_secret( fg = Colors.GREEN if all(statuses) else Colors.YELLOW text = "successfully" if all(statuses) else "partially" - spinner.write('') - spinner.text = click.style( - 'Secret(s) deleted {}.'.format(text), fg) + spinner.write("") + spinner.text = click.style("Secret(s) deleted {}.".format(text), fg) spinner.ok(click.style(icon, fg)) except Exception as e: - spinner.text = click.style( - 'Failed to delete secret(s): {}'.format(e), Colors.RED) + spinner.text = click.style("Failed to delete secret(s): {}".format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -149,6 +159,6 @@ def delete_secret( def _apply_delete(client: Client, result: Queue, secret: typing.Any) -> None: try: client.delete_secret(secret.metadata.name) - result.put((secret.metadata.name, True, 'Secret deleted successfully')) + result.put((secret.metadata.name, True, "Secret deleted successfully")) except Exception as e: result.put((secret.metadata.name, False, str(e))) diff --git a/riocli/secret/inspect.py b/riocli/secret/inspect.py index a6149aab..11b6c362 100644 --- a/riocli/secret/inspect.py +++ b/riocli/secret/inspect.py @@ -21,14 +21,19 @@ @click.command( - 'inspect', + "inspect", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--format', '-f', 'format_type', default='yaml', - type=click.Choice(['json', 'yaml'], case_sensitive=False)) -@click.argument('secret-name', type=str) +@click.option( + "--format", + "-f", + "format_type", + default="yaml", + type=click.Choice(["json", "yaml"], case_sensitive=False), +) +@click.argument("secret-name", type=str) def inspect_secret(format_type: str, secret_name: str) -> None: """ Inspect a secret diff --git a/riocli/secret/list.py b/riocli/secret/list.py index 521e742f..cbfde8de 100644 --- a/riocli/secret/list.py +++ b/riocli/secret/list.py @@ -23,20 +23,27 @@ @click.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--label', '-l', 'labels', multiple=True, type=click.STRING, - default=(), help='Filter the deployment list by labels') +@click.option( + "--label", + "-l", + "labels", + multiple=True, + type=click.STRING, + default=(), + help="Filter the deployment list by labels", +) def list_secrets(labels: typing.List[str]) -> None: """ List the secrets in the selected project """ try: client = new_v2_client(with_project=True) - secrets = client.list_secrets(query={'labelSelector': labels}) + secrets = client.list_secrets(query={"labelSelector": labels}) secrets = sorted(secrets, key=lambda s: s.metadata.name.lower()) _display_secret_list(secrets, show_header=True) except Exception as e: @@ -45,14 +52,21 @@ def list_secrets(labels: typing.List[str]) -> None: def _display_secret_list( - secrets: typing.List[munch.Munch], - show_header: bool = True, + secrets: typing.List[munch.Munch], + show_header: bool = True, ) -> None: headers = [] if show_header: - headers = ('ID', 'Name', 'Created At', 'Creator') + headers = ("ID", "Name", "Created At", "Creator") - data = [ [secret.metadata.guid, secret.metadata.name, - secret.metadata.createdAt, secret.metadata.creatorGUID] for secret in secrets ] + data = [ + [ + secret.metadata.guid, + secret.metadata.name, + secret.metadata.createdAt, + secret.metadata.creatorGUID, + ] + for secret in secrets + ] tabulate_data(data, headers) diff --git a/riocli/secret/util.py b/riocli/secret/util.py index 72093489..bd4acfaf 100644 --- a/riocli/secret/util.py +++ b/riocli/secret/util.py @@ -22,17 +22,22 @@ def fetch_secrets( - client: Client, - secret_name_or_regex: str, - include_all: bool, + client: Client, + secret_name_or_regex: str, + include_all: bool, ) -> List[Munch]: secrets = client.list_secrets() result = [] for secret in secrets: - if (include_all or secret_name_or_regex == secret.metadata.name or - secret_name_or_regex == secret.metadata.guid or - (secret_name_or_regex not in secret.metadata.name and - re.search(r'^{}$'.format(secret_name_or_regex), secret.metadata.name))): + if ( + include_all + or secret_name_or_regex == secret.metadata.name + or secret_name_or_regex == secret.metadata.guid + or ( + secret_name_or_regex not in secret.metadata.name + and re.search(r"^{}$".format(secret_name_or_regex), secret.metadata.name) + ) + ): result.append(secret) return result @@ -41,10 +46,12 @@ def fetch_secrets( def print_secrets_for_confirmation(secrets: List[Munch]): data = [] for secret in secrets: - data.append([ - secret.metadata.name, - secret.metadata.creatorGUID, - secret.metadata.createdAt, - ]) - - tabulate_data(data, ['Name', 'Creator', 'CreatedAt']) + data.append( + [ + secret.metadata.name, + secret.metadata.creatorGUID, + secret.metadata.createdAt, + ] + ) + + tabulate_data(data, ["Name", "Creator", "CreatedAt"]) diff --git a/riocli/shell/__init__.py b/riocli/shell/__init__.py index de867f8e..3e52ff5e 100644 --- a/riocli/shell/__init__.py +++ b/riocli/shell/__init__.py @@ -39,11 +39,11 @@ def shell(ctx: click.Context): @click.command( - 'repl', + "repl", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, - hidden=True + hidden=True, ) @click.pass_context def deprecated_repl(ctx: click.Context): @@ -59,7 +59,7 @@ def start_shell(ctx: click.Context): try: repl(click.get_current_context(), prompt_kwargs=prompt_config) except Exception as e: - click.secho(str(e), fg='red') + click.secho(str(e), fg="red") else: break @@ -67,11 +67,11 @@ def start_shell(ctx: click.Context): def _parse_config(config: Configuration) -> dict: history_path = os.path.join(click.get_app_dir(config.APP_NAME), "history") default_prompt_kwargs = { - 'history': ThreadedHistory(FileHistory(history_path)), - 'message': prompt_callback, - 'enable_suspend': True + "history": ThreadedHistory(FileHistory(history_path)), + "message": prompt_callback, + "enable_suspend": True, } - shell_config = config.data.get('shell', {}) + shell_config = config.data.get("shell", {}) return {**default_prompt_kwargs, **shell_config} diff --git a/riocli/shell/prompt.py b/riocli/shell/prompt.py index 8deafbef..86816c18 100644 --- a/riocli/shell/prompt.py +++ b/riocli/shell/prompt.py @@ -16,6 +16,6 @@ @click.pass_context def prompt_callback(ctx: click.Context) -> str: - organization_name = ctx.obj.data['organization_name'] - project_name = ctx.obj.data['project_name'] - return '{}:{} > '.format(organization_name, project_name) + organization_name = ctx.obj.data["organization_name"] + project_name = ctx.obj.data["project_name"] + return "{}:{} > ".format(organization_name, project_name) diff --git a/riocli/static_route/create.py b/riocli/static_route/create.py index 45dc61ad..c8bee10a 100644 --- a/riocli/static_route/create.py +++ b/riocli/static_route/create.py @@ -20,12 +20,12 @@ @click.command( - 'create', + "create", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('name', type=str) +@click.argument("name", type=str) @with_spinner(text="Creating static route...") def create_static_route(name: str, spinner=None) -> None: """Create a new static route @@ -37,14 +37,16 @@ def create_static_route(name: str, spinner=None) -> None: """ try: client = new_v2_client(with_project=True) - payload = { - "metadata": {"name": name} - } + payload = {"metadata": {"name": name}} route = client.create_static_route(payload) spinner.text = click.style( - 'Static Route created successfully for URL {}'.format(route.spec.url), fg=Colors.GREEN) + "Static Route created successfully for URL {}".format(route.spec.url), + fg=Colors.GREEN, + ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style('Failed to create static route: {}'.format(e), fg=Colors.RED) + spinner.text = click.style( + "Failed to create static route: {}".format(e), fg=Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e diff --git a/riocli/static_route/delete.py b/riocli/static_route/delete.py index 57898751..39909796 100644 --- a/riocli/static_route/delete.py +++ b/riocli/static_route/delete.py @@ -28,26 +28,38 @@ @click.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--force', '-f', '--silent', is_flag=True, default=False, - help='Skip confirmation') -@click.option('-a', '--all', 'delete_all', is_flag=True, default=False, - help='Deletes all static routes in the project') -@click.option('--workers', '-w', - help="Number of parallel workers while running delete static route " - "command. Defaults to 10.", type=int, default=10) -@click.argument('route-name-or-regex', type=str, default="") +@click.option( + "--force", "-f", "--silent", is_flag=True, default=False, help="Skip confirmation" +) +@click.option( + "-a", + "--all", + "delete_all", + is_flag=True, + default=False, + help="Deletes all static routes in the project", +) +@click.option( + "--workers", + "-w", + help="Number of parallel workers while running delete static route " + "command. Defaults to 10.", + type=int, + default=10, +) +@click.argument("route-name-or-regex", type=str, default="") @with_spinner(text="Deleting static route...") def delete_static_route( - route_name_or_regex: str, - force: bool, - delete_all: bool = False, - workers: int = 10, - spinner=None, + route_name_or_regex: str, + force: bool, + delete_all: bool = False, + workers: int = 10, + spinner=None, ) -> None: """Delete one or more static routes with a name or a regex pattern. @@ -88,7 +100,8 @@ def delete_static_route( routes = fetch_static_routes(client, route_name_or_regex, delete_all) except Exception as e: spinner.text = click.style( - 'Failed to delete static route(s): {}'.format(e), Colors.RED) + "Failed to delete static route(s): {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -100,34 +113,35 @@ def delete_static_route( with spinner.hidden(): print_routes_for_confirmation(routes) - spinner.write('') + spinner.write("") if not force: with spinner.hidden(): - click.confirm('Do you want to delete the above static route(s)?', abort=True) - spinner.write('') + click.confirm("Do you want to delete the above static route(s)?", abort=True) + spinner.write("") try: f = functools.partial(_apply_delete, client) - result = apply_func_with_result(f=f, items=routes, workers=workers, key=lambda x: x[0]) + result = apply_func_with_result( + f=f, items=routes, workers=workers, key=lambda x: x[0] + ) data, statuses = [], [] for name, status, msg in result: fg = Colors.GREEN if status else Colors.RED icon = Symbols.SUCCESS if status else Symbols.ERROR statuses.append(status) - data.append([ - click.style(name, fg), - click.style('{} {}'.format(icon, msg), fg) - ]) + data.append( + [click.style(name, fg), click.style("{} {}".format(icon, msg), fg)] + ) with spinner.hidden(): - tabulate_data(data, headers=['Name', 'Status']) + tabulate_data(data, headers=["Name", "Status"]) # When no route is deleted, raise an exception. if not any(statuses): - spinner.write('') - spinner.text = click.style('Failed to delete static route(s).', Colors.RED) + spinner.write("") + spinner.text = click.style("Failed to delete static route(s).", Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) @@ -135,13 +149,13 @@ def delete_static_route( fg = Colors.GREEN if all(statuses) else Colors.YELLOW text = "successfully" if all(statuses) else "partially" - spinner.write('') - spinner.text = click.style( - 'Static route(s) deleted {}.'.format(text), fg) + spinner.write("") + spinner.text = click.style("Static route(s) deleted {}.".format(text), fg) spinner.ok(click.style(icon, fg)) except Exception as e: spinner.text = click.style( - 'Failed to delete static route(s): {}'.format(e), Colors.RED) + "Failed to delete static route(s): {}".format(e), Colors.RED + ) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -149,6 +163,6 @@ def delete_static_route( def _apply_delete(client: Client, result: Queue, route: typing.Any) -> None: try: client.delete_static_route(name=route.metadata.name) - result.put((route.metadata.name, True, 'Static route deleted successfully')) + result.put((route.metadata.name, True, "Static route deleted successfully")) except Exception as e: result.put((route.metadata.name, False, str(e))) diff --git a/riocli/static_route/inspect.py b/riocli/static_route/inspect.py index eb0d6cc4..e9a5a917 100644 --- a/riocli/static_route/inspect.py +++ b/riocli/static_route/inspect.py @@ -21,18 +21,22 @@ @click.command( - 'inspect', + "inspect", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--format', '-f', 'format_type', - type=click.Choice(['json', 'yaml'], case_sensitive=True), - default='yaml') -@click.argument('static-route', type=str) +@click.option( + "--format", + "-f", + "format_type", + type=click.Choice(["json", "yaml"], case_sensitive=True), + default="yaml", +) +@click.argument("static-route", type=str) def inspect_static_route( - format_type: str, - static_route: str, + format_type: str, + static_route: str, ) -> None: """Print the details of a static route. @@ -46,5 +50,3 @@ def inspect_static_route( except Exception as e: click.secho(str(e), fg=Colors.RED) raise SystemExit(1) from e - - diff --git a/riocli/static_route/list.py b/riocli/static_route/list.py index 2a1cf01a..e0c8646f 100644 --- a/riocli/static_route/list.py +++ b/riocli/static_route/list.py @@ -24,13 +24,20 @@ @click.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--label', '-l', 'labels', multiple=True, type=click.STRING, - default=(), help='Filter the deployment list by labels') +@click.option( + "--label", + "-l", + "labels", + multiple=True, + type=click.STRING, + default=(), + help="Filter the deployment list by labels", +) def list_static_routes(labels: typing.List[str]) -> None: """List the static routes in the current project. @@ -45,7 +52,7 @@ def list_static_routes(labels: typing.List[str]) -> None: """ try: client = new_v2_client(with_project=True) - routes = client.list_static_routes(query={'labelSelector': labels}) + routes = client.list_static_routes(query={"labelSelector": labels}) _display_routes_list(routes) except Exception as e: click.secho(str(e), fg=Colors.RED) @@ -53,16 +60,18 @@ def list_static_routes(labels: typing.List[str]) -> None: def _display_routes_list(routes: List[munch.Munch]) -> None: - headers = ['Route ID', 'Name', 'URL', 'Creator', 'CreatedAt'] + headers = ["Route ID", "Name", "URL", "Creator", "CreatedAt"] data = [] for route in routes: - data.append([ - route.metadata.guid, - route.metadata.name, - route.spec.url, - route.metadata.creatorGUID, - route.metadata.createdAt, - ]) + data.append( + [ + route.metadata.guid, + route.metadata.name, + route.spec.url, + route.metadata.creatorGUID, + route.metadata.createdAt, + ] + ) tabulate_data(data, headers) diff --git a/riocli/static_route/model.py b/riocli/static_route/model.py index af84a603..e5c9715c 100644 --- a/riocli/static_route/model.py +++ b/riocli/static_route/model.py @@ -43,6 +43,6 @@ def delete(self, *args, **kwargs) -> None: short_id = Configuration().organization_short_id try: - client.delete_static_route(f'{self.metadata.name}-{short_id}') + client.delete_static_route(f"{self.metadata.name}-{short_id}") except HttpNotFoundError: raise ResourceNotFound diff --git a/riocli/static_route/open.py b/riocli/static_route/open.py index 7d6d7bb4..54d11182 100644 --- a/riocli/static_route/open.py +++ b/riocli/static_route/open.py @@ -19,18 +19,18 @@ @click.command( - 'open', + "open", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('static-route', type=str) +@click.argument("static-route", type=str) def open_static_route(static_route) -> None: """Open a static route in the default browser.""" try: client = new_v2_client() route = client.get_static_route(static_route) - click.launch(url='https://{}'.format(route.spec.url), wait=False) + click.launch(url="https://{}".format(route.spec.url), wait=False) except Exception as e: click.secho(str(e), fg=Colors.RED) raise SystemExit(1) diff --git a/riocli/static_route/util.py b/riocli/static_route/util.py index 5d794fba..04a317d5 100644 --- a/riocli/static_route/util.py +++ b/riocli/static_route/util.py @@ -24,7 +24,7 @@ class StaticRouteNotFound(Exception): def __init__(self): - super().__init__('static route not found') + super().__init__("static route not found") def find_static_route_guid(client: Client, name: str) -> str: @@ -36,17 +36,22 @@ def find_static_route_guid(client: Client, name: str) -> str: def fetch_static_routes( - client: Client, - route_name_or_regex: str, - include_all: bool, + client: Client, + route_name_or_regex: str, + include_all: bool, ) -> List[Munch]: routes = client.list_static_routes() result = [] for route in routes: - if (include_all or route_name_or_regex == route.metadata.name or - route_name_or_regex == route.metadata.guid or - (route_name_or_regex not in route.metadata.name and - re.search(r'^{}$'.format(route_name_or_regex), route.metadata.name))): + if ( + include_all + or route_name_or_regex == route.metadata.name + or route_name_or_regex == route.metadata.guid + or ( + route_name_or_regex not in route.metadata.name + and re.search(r"^{}$".format(route_name_or_regex), route.metadata.name) + ) + ): result.append(route) return result @@ -55,10 +60,12 @@ def fetch_static_routes( def print_routes_for_confirmation(routes: List[Munch]): data = [] for route in routes: - data.append([ - route.metadata.name, - route.metadata.creatorGUID, - route.metadata.createdAt, - ]) + data.append( + [ + route.metadata.name, + route.metadata.creatorGUID, + route.metadata.createdAt, + ] + ) - tabulate_data(data, ['Name', 'Creator', 'CreatedAt']) + tabulate_data(data, ["Name", "Creator", "CreatedAt"]) diff --git a/riocli/usergroup/delete.py b/riocli/usergroup/delete.py index 0f9b87ca..3fd6af28 100644 --- a/riocli/usergroup/delete.py +++ b/riocli/usergroup/delete.py @@ -23,23 +23,30 @@ @click.command( - 'delete', + "delete", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--force', '-f', '--silent', 'force', is_flag=True, - default=False, help='Skip confirmation') -@click.argument('group-name') +@click.option( + "--force", + "-f", + "--silent", + "force", + is_flag=True, + default=False, + help="Skip confirmation", +) +@click.argument("group-name") @click.pass_context @with_spinner(text="Deleting user group...") @name_to_guid def delete_usergroup( - ctx: click.Context, - group_name: str, - group_guid: str, - force: bool, - spinner: Yaspin = None, + ctx: click.Context, + group_name: str, + group_guid: str, + force: bool, + spinner: Yaspin = None, ) -> None: """Delete a usergroup from current organization. @@ -48,15 +55,17 @@ def delete_usergroup( """ if not force: with spinner.hidden(): - click.confirm('Deleting usergroup {} ({})'.format(group_name, group_guid), abort=True) + click.confirm( + "Deleting usergroup {} ({})".format(group_name, group_guid), abort=True + ) try: client = new_client() - org_guid = ctx.obj.data.get('organization_id') + org_guid = ctx.obj.data.get("organization_id") client.delete_usergroup(org_guid, group_guid) - spinner.text = click.style('User group deleted successfully.', fg=Colors.GREEN) + spinner.text = click.style("User group deleted successfully.", fg=Colors.GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style('Failed to delete usergroup: {}'.format(e), Colors.RED) + spinner.text = click.style("Failed to delete usergroup: {}".format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e diff --git a/riocli/usergroup/inspect.py b/riocli/usergroup/inspect.py index 71586979..e1ef4e57 100644 --- a/riocli/usergroup/inspect.py +++ b/riocli/usergroup/inspect.py @@ -24,17 +24,24 @@ @click.command( - 'inspect', + "inspect", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--format', '-f', 'format_type', default='yaml', - type=click.Choice(['json', 'yaml'], case_sensitive=False)) -@click.argument('group-name') +@click.option( + "--format", + "-f", + "format_type", + default="yaml", + type=click.Choice(["json", "yaml"], case_sensitive=False), +) +@click.argument("group-name") @click.pass_context @name_to_guid -def inspect_usergroup(ctx: click.Context, format_type: str, group_name: str, group_guid: str, spinner=None) -> None: +def inspect_usergroup( + ctx: click.Context, format_type: str, group_name: str, group_guid: str, spinner=None +) -> None: """Print the details of a usergroup You choose the format of the output using the ``--format`` flag. @@ -42,7 +49,7 @@ def inspect_usergroup(ctx: click.Context, format_type: str, group_name: str, gro """ try: client = new_client() - org_guid = ctx.obj.data.get('organization_id') + org_guid = ctx.obj.data.get("organization_id") usergroup = client.get_usergroup(org_guid, group_guid) inspect_with_format(to_manifest(usergroup, org_guid), format_type) except Exception as e: @@ -54,24 +61,29 @@ def to_manifest(usergroup: UserGroup, org_guid: str) -> typing.Dict: """ Transform a usergroup resource to a rio apply manifest construct """ - role_map = {i['projectGUID']: i['groupRole'] for i in (usergroup.role_in_projects or [])} + role_map = { + i["projectGUID"]: i["groupRole"] for i in (usergroup.role_in_projects or []) + } members = {m.email_id for m in usergroup.members} admins = {a.email_id for a in usergroup.admins} - projects = [{'name': p.name, 'role': role_map.get(p.guid)} - for p in (usergroup.projects or []) if p.guid in role_map] + projects = [ + {"name": p.name, "role": role_map.get(p.guid)} + for p in (usergroup.projects or []) + if p.guid in role_map + ] return { - 'apiVersion': 'api.rapyuta.io/v2', - 'kind': 'UserGroup', - 'metadata': { - 'name': usergroup.name, - 'creator': usergroup.creator, - 'organization': org_guid, + "apiVersion": "api.rapyuta.io/v2", + "kind": "UserGroup", + "metadata": { + "name": usergroup.name, + "creator": usergroup.creator, + "organization": org_guid, }, - 'spec': { - 'description': usergroup.description, - 'members': [{'emailID': m} for m in list(members - admins)], - 'admins': [{'emailID': a} for a in list(admins)], - 'projects': projects, + "spec": { + "description": usergroup.description, + "members": [{"emailID": m} for m in list(members - admins)], + "admins": [{"emailID": a} for a in list(admins)], + "projects": projects, }, } diff --git a/riocli/usergroup/list.py b/riocli/usergroup/list.py index 458050f6..7a90c84c 100644 --- a/riocli/usergroup/list.py +++ b/riocli/usergroup/list.py @@ -23,7 +23,7 @@ @click.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, @@ -33,7 +33,7 @@ def list_usergroup(ctx: click.Context) -> None: """List all user groups in current organization.""" try: client = new_client() - org_guid = ctx.obj.data.get('organization_id') + org_guid = ctx.obj.data.get("organization_id") user_groups = client.list_usergroups(org_guid) _display_usergroup_list(user_groups) except Exception as e: @@ -44,9 +44,7 @@ def list_usergroup(ctx: click.Context) -> None: def _display_usergroup_list(usergroups: typing.Any, show_header: bool = True): headers = [] if show_header: - headers = ( - 'ID', 'Name', 'Creator', 'Members', 'Projects', 'Description' - ) + headers = ("ID", "Name", "Creator", "Members", "Projects", "Description") data = [ [ @@ -55,7 +53,7 @@ def _display_usergroup_list(usergroups: typing.Any, show_header: bool = True): group.creator, len(group.members) if group.members else 0, len(group.projects) if group.projects else 0, - group.description + group.description, ] for group in usergroups ] diff --git a/riocli/usergroup/model.py b/riocli/usergroup/model.py index ff0b0a14..9e0b76cc 100644 --- a/riocli/usergroup/model.py +++ b/riocli/usergroup/model.py @@ -22,8 +22,8 @@ from riocli.organization.utils import get_organization_details from riocli.usergroup.util import UserGroupNotFound, find_usergroup_guid -USER_GUID = 'guid' -USER_EMAIL = 'emailID' +USER_GUID = "guid" +USER_EMAIL = "emailID" class UserGroup(Model): @@ -52,16 +52,20 @@ def apply(self, *args, **kwargs) -> ApplyResult: organization_details = get_organization_details(organization_id) user_projects = v2client.list_projects(organization_id) - self.project_name_to_guid_map = {p['metadata']['name']: p['metadata']['guid'] for p in user_projects} - self.user_email_to_guid_map = {user[USER_EMAIL]: user[USER_GUID] for user in organization_details['users']} + self.project_name_to_guid_map = { + p["metadata"]["name"]: p["metadata"]["guid"] for p in user_projects + } + self.user_email_to_guid_map = { + user[USER_EMAIL]: user[USER_GUID] for user in organization_details["users"] + } sanitized = self._sanitize() payload = self._modify_payload(sanitized) if not existing: try: - payload['spec']['name'] = sanitized['metadata']['name'] - v1client.create_usergroup(self.metadata.organization, payload['spec']) + payload["spec"]["name"] = sanitized["metadata"]["name"] + v1client.create_usergroup(self.metadata.organization, payload["spec"]) return ApplyResult.CREATED except Exception as e: raise e @@ -85,46 +89,48 @@ def delete(self, *args, **kwargs) -> None: def _sanitize(self) -> typing.Dict: u = unmunchify(self) - u.pop('project_name_to_guid_map', None) - u.pop('user_email_to_guid_map', None) + u.pop("project_name_to_guid_map", None) + u.pop("user_email_to_guid_map", None) return u def _modify_payload(self, group: typing.Dict) -> typing.Dict: - group['spec']['userGroupRoleInProjects'] = [] - for entity in ('members', 'admins'): - for u in group['spec'].get(entity, []): + group["spec"]["userGroupRoleInProjects"] = [] + for entity in ("members", "admins"): + for u in group["spec"].get(entity, []): if USER_GUID in u: continue u[USER_GUID] = self.user_email_to_guid_map.get(u[USER_EMAIL]) u.pop(USER_EMAIL) - for p in group['spec'].get('projects', []): - if 'guid' not in p: - p['guid'] = self.project_name_to_guid_map.get(p['name']) - p.pop('name') + for p in group["spec"].get("projects", []): + if "guid" not in p: + p["guid"] = self.project_name_to_guid_map.get(p["name"]) + p.pop("name") - if 'role' in p: - group['spec']['userGroupRoleInProjects'].append({ - 'projectGUID': p['guid'], - 'groupRole': p['role'], - }) - p.pop('role') + if "role" in p: + group["spec"]["userGroupRoleInProjects"].append( + { + "projectGUID": p["guid"], + "groupRole": p["role"], + } + ) + p.pop("role") return group @staticmethod def _generate_update_payload(old: typing.Any, new: typing.Dict) -> typing.Dict: payload = { - 'name': old.name, - 'guid': old.guid, - 'description': new['spec']['description'], - 'update': { - 'members': {'add': [], 'remove': []}, - 'projects': {'add': [], 'remove': []}, - 'admins': {'add': [], 'remove': []} + "name": old.name, + "guid": old.guid, + "description": new["spec"]["description"], + "update": { + "members": {"add": [], "remove": []}, + "projects": {"add": [], "remove": []}, + "admins": {"add": [], "remove": []}, }, - 'userGroupRoleInProjects': new['spec'].get('userGroupRoleInProjects', []), + "userGroupRoleInProjects": new["spec"].get("userGroupRoleInProjects", []), } entity_sets = { @@ -139,41 +145,45 @@ def _generate_update_payload(old: typing.Any, new: typing.Dict) -> typing.Dict: "projects": { "old": set(), "new": set(), - } + }, } - for entity in ('members', 'projects', 'admins'): + for entity in ("members", "projects", "admins"): # Assure that the group creator is not removed - old_set = {i.guid for i in (getattr(old, entity) or []) if i.guid != old.creator} - new_set = {i['guid'] for i in new['spec'].get(entity, [])} + old_set = { + i.guid for i in (getattr(old, entity) or []) if i.guid != old.creator + } + new_set = {i["guid"] for i in new["spec"].get(entity, [])} entity_sets[entity]["old"] = old_set entity_sets[entity]["new"] = new_set - for entity in ('projects', 'admins'): - new = entity_sets[entity]['new'] - old = entity_sets[entity]['old'] + for entity in ("projects", "admins"): + new = entity_sets[entity]["new"] + old = entity_sets[entity]["old"] added = new - old removed = old - new - payload['update'][entity]['add'] = [{'guid': guid} for guid in added] - payload['update'][entity]['remove'] = [{'guid': guid} for guid in removed] + payload["update"][entity]["add"] = [{"guid": guid} for guid in added] + payload["update"][entity]["remove"] = [{"guid": guid} for guid in removed] # Handle special cases in the members section separately - new_members = entity_sets['members']['new'] - old_members = entity_sets['members']['old'] + new_members = entity_sets["members"]["new"] + old_members = entity_sets["members"]["old"] added_members = new_members - old_members removed_members = old_members - new_members # Additional handling to avoid active admins becoming a part of # removed members set which leads to their removal altogether. - removed_members = removed_members - entity_sets['admins']['new'] + removed_members = removed_members - entity_sets["admins"]["new"] - payload['update']['members']['add'] = [{'guid': guid} for guid in added_members] - payload['update']['members']['remove'] = [{'guid': guid} for guid in removed_members] + payload["update"]["members"]["add"] = [{"guid": guid} for guid in added_members] + payload["update"]["members"]["remove"] = [ + {"guid": guid} for guid in removed_members + ] # This is a special case where admins are not added to the membership list # And as a consequence they don't show up in the group. This will fix that. - payload['update']['members']['add'].extend(payload['update']['admins']['add']) + payload["update"]["members"]["add"].extend(payload["update"]["admins"]["add"]) return payload diff --git a/riocli/usergroup/util.py b/riocli/usergroup/util.py index 9120df9e..f7267883 100644 --- a/riocli/usergroup/util.py +++ b/riocli/usergroup/util.py @@ -31,13 +31,13 @@ def decorated(*args: typing.Any, **kwargs: typing.Any) -> None: click.secho(str(e), fg=Colors.RED) raise SystemExit(1) - group_name = kwargs.pop('group_name') + group_name = kwargs.pop("group_name") group_guid = None ctx = args[0] - org_guid = ctx.obj.data.get('organization_id') + org_guid = ctx.obj.data.get("organization_id") - if group_name.startswith('group-'): + if group_name.startswith("group-"): group_guid = group_name group_name = None @@ -51,8 +51,8 @@ def decorated(*args: typing.Any, **kwargs: typing.Any) -> None: click.secho(str(e), fg=Colors.RED) raise SystemExit(1) - kwargs['group_name'] = group_name - kwargs['group_guid'] = group_guid + kwargs["group_name"] = group_name + kwargs["group_guid"] = group_guid f(*args, **kwargs) return decorated @@ -79,6 +79,6 @@ def find_usergroup_guid(client: Client, org_guid, group_name: str) -> str: class UserGroupNotFound(Exception): - def __init__(self, message='usergroup not found!'): + def __init__(self, message="usergroup not found!"): self.message = message super().__init__(self.message) diff --git a/riocli/utils/__init__.py b/riocli/utils/__init__.py index 64040bab..bad06788 100644 --- a/riocli/utils/__init__.py +++ b/riocli/utils/__init__.py @@ -41,18 +41,17 @@ class Singleton(type): def __call__(cls, *args, **kwargs): if cls not in cls._instances: - cls._instances[cls] = super( - Singleton, cls).__call__(*args, **kwargs) + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] def inspect_with_format(obj: typing.Any, format_type: str): - if format_type == 'json': + if format_type == "json": click.echo_via_pager(json.dumps(obj, indent=4)) - elif format_type == 'yaml': + elif format_type == "yaml": click.echo_via_pager(yaml.dump(obj, allow_unicode=True)) else: - raise Exception('Invalid format') + raise Exception("Invalid format") def dump_all_yaml(objs: typing.List): @@ -60,11 +59,7 @@ def dump_all_yaml(objs: typing.List): Dump multiple documents as YAML separated by triple dash (---) """ click.echo_via_pager( - yaml.safe_dump_all( - documents=objs, - allow_unicode=True, - explicit_start=True - ) + yaml.safe_dump_all(documents=objs, allow_unicode=True, explicit_start=True) ) @@ -77,7 +72,7 @@ def run_bash_with_return_code(cmd, bg=False) -> (str, int): ret_code = output.returncode else: output = subprocess.run(cmd_parts, stdout=subprocess.PIPE) - stdout = output.stdout.decode('utf-8') + stdout = output.stdout.decode("utf-8") ret_code = output.returncode return stdout.strip(), ret_code @@ -92,28 +87,27 @@ def run_bash(cmd, bg=False): bg_output = subprocess.Popen(cmd_parts) output = str(bg_output.stdout).strip() else: - output = subprocess.run(cmd_parts, stdout=subprocess.PIPE, - check=True).stdout.decode('utf-8') + output = subprocess.run( + cmd_parts, stdout=subprocess.PIPE, check=True + ).stdout.decode("utf-8") return output.strip() riocli_group_opts = { - 'invoke_without_command': True, - 'cls': HelpColorsGroup, - 'help_headers_color': 'yellow', - 'help_options_color': 'green' + "invoke_without_command": True, + "cls": HelpColorsGroup, + "help_headers_color": "yellow", + "help_options_color": "green", } def random_string(letter_count, digit_count): - str1 = ''.join( - (random.choice(string.ascii_letters) for x in range(letter_count))) - str1 += ''.join((random.choice(string.digits) for x in range(digit_count))) + str1 = "".join((random.choice(string.ascii_letters) for x in range(letter_count))) + str1 += "".join((random.choice(string.digits) for x in range(digit_count))) sam_list = list(str1) # it converts the string to list. - random.shuffle( - sam_list) # It uses a random.shuffle() function to shuffle the string. - final_string = ''.join(sam_list) + random.shuffle(sam_list) # It uses a random.shuffle() function to shuffle the string. + final_string = "".join(sam_list) return final_string @@ -145,14 +139,13 @@ def is_valid_uuid(uuid_to_test, version=4): return str(uuid_obj) == uuid_to_test -def tabulate_data(data: typing.List[typing.List], - headers: typing.List[str] = None): +def tabulate_data(data: typing.List[typing.List], headers: typing.List[str] = None): """ Prints data in tabular format """ # https://github.com/astanin/python-tabulate#table-format - table_format = 'simple' - header_foreground = 'yellow' + table_format = "simple" + header_foreground = "yellow" if headers: headers = [click.style(h, fg=header_foreground) for h in headers] @@ -160,7 +153,7 @@ def tabulate_data(data: typing.List[typing.List], click.echo(tabulate(data, headers=headers, tablefmt=table_format)) -def print_separator(color: str = 'blue'): +def print_separator(color: str = "blue"): """ Prints a separator """ @@ -169,19 +162,17 @@ def print_separator(color: str = 'blue'): def is_pip_installation() -> bool: - return 'python' in sys.executable + return "python" in sys.executable def check_for_updates(current_version: typing.Text) -> typing.Tuple[bool, typing.Text]: try: - package_info = requests.get( - 'https://pypi.org/pypi/rapyuta-io-cli/json').json() + package_info = requests.get("https://pypi.org/pypi/rapyuta-io-cli/json").json() except Exception as e: - click.secho('Failed to fetch upstream package info: {}'.format(e), - fg=Colors.RED) + click.secho("Failed to fetch upstream package info: {}".format(e), fg=Colors.RED) raise SystemExit(1) from e - upstream_version = package_info.get('info', {}).get('version') + upstream_version = package_info.get("info", {}).get("version") current_version = semver.Version.parse(current_version) available = semver.Version.parse(upstream_version).compare(current_version) @@ -190,26 +181,26 @@ def check_for_updates(current_version: typing.Text) -> typing.Tuple[bool, typing def pip_install_cli( - version: str, - force_reinstall: bool = False, + version: str, + force_reinstall: bool = False, ) -> subprocess.CompletedProcess: """ Installs the given rapyuta-io-cli version using pip """ if not version: - raise ValueError('version cannot by empty.') + raise ValueError("version cannot by empty.") try: semver.Version.parse(version) except ValueError as err: raise err - package_name = 'rapyuta-io-cli=={}'.format(version) + package_name = "rapyuta-io-cli=={}".format(version) # https://pip.pypa.io/en/latest/user_guide/#using-pip-from-your-program - command = [sys.executable, '-m', 'pip', 'install', package_name] + command = [sys.executable, "-m", "pip", "install", package_name] if force_reinstall: - command.append('--force-reinstall') + command.append("--force-reinstall") return subprocess.run(command, check=True) @@ -219,38 +210,36 @@ def update_appimage(version: str): Updates the AppImage locally """ if not version: - raise ValueError('version cannot be empty') + raise ValueError("version cannot be empty") # URL to get the latest release metadata - url = 'https://api.github.com/repos/rapyuta-robotics/rapyuta-io-cli/releases/latest' + url = "https://api.github.com/repos/rapyuta-robotics/rapyuta-io-cli/releases/latest" try: response = requests.get(url) data = munchify(response.json()) except Exception as e: - click.secho('Failed to fetch release info: {}'.format(e), - fg=Colors.RED) + click.secho("Failed to fetch release info: {}".format(e), fg=Colors.RED) raise SystemExit(1) from e asset = None - for a in data.get('assets', []): - if 'AppImage' in a.name and version in a.name: + for a in data.get("assets", []): + if "AppImage" in a.name and version in a.name: asset = a break if asset is None: - raise Exception( - 'Failed to retrieve the download URL for the latest AppImage') + raise Exception("Failed to retrieve the download URL for the latest AppImage") # Download the AppImage try: response = requests.get(asset.browser_download_url) except Exception as e: - raise Exception('Failed to download the new version: {}'.format(e)) + raise Exception("Failed to download the new version: {}".format(e)) with TemporaryDirectory() as tmp: # Save the binary in a temp dir - save_to = Path(tmp) / 'rio' + save_to = Path(tmp) / "rio" save_to.write_bytes(response.content) os.chmod(save_to, 0o755) # Now replace the current executable with the new file @@ -258,7 +247,10 @@ def update_appimage(version: str): os.remove(sys.executable) move(save_to, sys.executable) except OSError as e: - click.secho('{} Please consider running as a root user.'.format(Symbols.WARNING), fg=Colors.YELLOW) + click.secho( + "{} Please consider running as a root user.".format(Symbols.WARNING), + fg=Colors.YELLOW, + ) raise e except Exception as e: raise e @@ -279,7 +271,7 @@ def trim_prefix(name): if len(name) == 0 or name[len(name) - 1].isalnum(): return name - return trim_prefix(name[:len(name) - 1]) + return trim_prefix(name[: len(name) - 1]) def sanitize_label(name): @@ -290,9 +282,9 @@ def sanitize_label(name): name = trim_suffix(name) name = trim_prefix(name) - r = '' + r = "" for c in name: - if c.isalnum() or c in ['-', '_', '.']: + if c.isalnum() or c in ["-", "_", "."]: r = r + c return r @@ -300,5 +292,5 @@ def sanitize_label(name): def print_centered_text(text: str, color: str = Colors.YELLOW): col, _ = get_terminal_size() - text = click.style(f' {text} '.center(col, '-'), fg=color, bold=True) + text = click.style(f" {text} ".center(col, "-"), fg=color, bold=True) click.echo(text) diff --git a/riocli/utils/execute.py b/riocli/utils/execute.py index 2ab9afc1..099cfc1f 100644 --- a/riocli/utils/execute.py +++ b/riocli/utils/execute.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. import functools -import json import typing from concurrent.futures import ThreadPoolExecutor from queue import Queue @@ -21,15 +20,16 @@ from riocli.config import new_client + def run_on_device( - device_guid: str = None, - command: typing.List[str] = None, - user: str = 'root', - shell: str = '/bin/bash', - background: bool = False, - deployment: str = None, - exec_name: str = None, - device_name: str = None + device_guid: str = None, + command: typing.List[str] = None, + user: str = "root", + shell: str = "/bin/bash", + background: bool = False, + deployment: str = None, + exec_name: str = None, + device_name: str = None, ) -> str: client = new_client() @@ -41,27 +41,28 @@ def run_on_device( if devices: device = devices[0] else: - raise ValueError('Either `device_guid` or `device_name` must be specified') + raise ValueError("Either `device_guid` or `device_name` must be specified") if not device: - raise ValueError('Device not found or is not online') + raise ValueError("Device not found or is not online") if deployment and exec_name is None: - raise ValueError('The `exec_name` argument is required when `deployment` is specified') - + raise ValueError( + "The `exec_name` argument is required when `deployment` is specified" + ) + if not command: - raise ValueError('The `command` argument is required') + raise ValueError("The `command` argument is required") - cmd = ' '.join(command) + cmd = " ".join(command) if deployment: cmd = 'script -q -c "dectl exec {} -- {}"'.format(exec_name, cmd) return device.execute_command(Command(cmd, shell=shell, bg=background, runas=user)) + def apply_func( - f: typing.Callable, - items: typing.List[typing.Any], - workers: int = 5 + f: typing.Callable, items: typing.List[typing.Any], workers: int = 5 ) -> None: """Apply a function to a list of items in parallel @@ -74,18 +75,15 @@ def apply_func( workers : int The number of workers to use """ - with ThreadPoolExecutor( - max_workers=workers, - thread_name_prefix='exec' - ) as e: + with ThreadPoolExecutor(max_workers=workers, thread_name_prefix="exec") as e: e.map(f, items) def apply_func_with_result( - f: typing.Callable, - items: typing.List[typing.Any], - workers: int = 5, - key: typing.Callable = None + f: typing.Callable, + items: typing.List[typing.Any], + workers: int = 5, + key: typing.Callable = None, ) -> typing.List[typing.Any]: """Apply a function to a list of items in parallel and return the result diff --git a/riocli/utils/graph.py b/riocli/utils/graph.py index e1c7dd66..26f5439a 100644 --- a/riocli/utils/graph.py +++ b/riocli/utils/graph.py @@ -12,14 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from abc import ABC, abstractmethod import base64 import tempfile +from abc import ABC, abstractmethod from typing import Optional import click from graphviz import Digraph + class GraphVisualizer(ABC): @abstractmethod def node(self, key: str, label: Optional[str] = None) -> None: @@ -33,8 +34,9 @@ def edge(self, from_node: str, to_node: str) -> None: def visualize(self) -> None: pass + class Mermaid(GraphVisualizer): - def __init__(self, format: str = 'svg', direction: str = 'LR') -> None: + def __init__(self, format: str = "svg", direction: str = "LR") -> None: self._diagram = ["flowchart {}".format(direction)] self._format = format @@ -42,33 +44,41 @@ def node(self, key: str, label: Optional[str] = None) -> None: if label is None: label = key - self._diagram.append("\t{}[{}]".format( - self._mermaid_safe(key), label)) + self._diagram.append("\t{}[{}]".format(self._mermaid_safe(key), label)) def edge(self, from_node: str, to_node: str) -> None: - self._diagram.append("\t{} --> {}".format( - self._mermaid_safe(from_node), self._mermaid_safe(to_node), - )) + self._diagram.append( + "\t{} --> {}".format( + self._mermaid_safe(from_node), + self._mermaid_safe(to_node), + ) + ) def visualize(self) -> None: print("\n".join(self._diagram)) click.launch(self._mermaid_link()) def _mermaid_link(self): - diagram = "\n".join(self._diagram).encode('ascii') - data = base64.b64encode(diagram).decode('ascii') - return 'https://mermaid.ink/{}/{}'.format(self._format, data) + diagram = "\n".join(self._diagram).encode("ascii") + data = base64.b64encode(diagram).decode("ascii") + return "https://mermaid.ink/{}/{}".format(self._format, data) def _mermaid_safe(self, s: str) -> str: return s.replace(" ", "_") + class Graphviz(GraphVisualizer): - def __init__(self, name: Optional[str] = None, format: str = 'svg', - direction: str = 'TB', shape: str = 'box') -> None: + def __init__( + self, + name: Optional[str] = None, + format: str = "svg", + direction: str = "TB", + shape: str = "box", + ) -> None: self._graph = Digraph(name=name) self._graph.format = format - self._graph.attr('graph', overlap='False', rankdir=direction) - self._graph.attr('node', shape=shape) + self._graph.attr("graph", overlap="False", rankdir=direction) + self._graph.attr("node", shape=shape) def node(self, key: str, label: Optional[str] = None) -> None: self._graph.node(self._graphviz_safe(key), label=label) diff --git a/riocli/utils/selector.py b/riocli/utils/selector.py index fbf1bb9b..1752bff2 100644 --- a/riocli/utils/selector.py +++ b/riocli/utils/selector.py @@ -21,11 +21,11 @@ def show_selection( - ranger: Union[list, dict], - header: str = '', - prompt: str = 'Select the option', - show_keys: bool = True, - highlight_item: str = None, + ranger: Union[list, dict], + header: str = "", + prompt: str = "Select the option", + show_keys: bool = True, + highlight_item: str = None, ) -> Any: """ Show a selection prompt to the user. @@ -46,15 +46,15 @@ def show_selection( def _show_selection_list( - ranger: list, - header: str, - prompt: str, - highlight_item: Any = None, + ranger: list, + header: str, + prompt: str, + highlight_item: Any = None, ) -> Any: click.secho(header, fg=Colors.YELLOW) for idx, opt in enumerate(ranger): - fmt = '{}) {}'.format(idx + 1, opt) + fmt = "{}) {}".format(idx + 1, opt) if highlight_item is not None and opt == highlight_item: fmt = click.style(fmt, bold=True, italic=True) @@ -67,19 +67,19 @@ def _show_selection_list( def _show_selection_dict( - ranger: dict, - header: str, - prompt: str, - show_keys: bool = True, - highlight_item: Any = None, + ranger: dict, + header: str, + prompt: str, + show_keys: bool = True, + highlight_item: Any = None, ) -> Any: click.secho(header, fg=Colors.YELLOW) for idx, key in enumerate(ranger): if show_keys: - fmt = '{}) {} - {}'.format(idx + 1, key, ranger[key]) + fmt = "{}) {} - {}".format(idx + 1, key, ranger[key]) else: - fmt = '{}) {}'.format(idx + 1, ranger[key]) + fmt = "{}) {}".format(idx + 1, ranger[key]) if highlight_item is not None and key == highlight_item: fmt = click.style(fmt, bold=True, italic=True) diff --git a/riocli/utils/spinner.py b/riocli/utils/spinner.py index ee753d96..6d4c2bcd 100644 --- a/riocli/utils/spinner.py +++ b/riocli/utils/spinner.py @@ -26,7 +26,7 @@ class DummySpinner: """ def __init__(self, *args, **kwargs): - self.text = '' + self.text = "" def write(self, text): click.echo(text) @@ -34,11 +34,11 @@ def write(self, text): def hidden(self): return self - def fail(self, text='FAIL'): - click.echo('{} {}'.format(text, self.text)) + def fail(self, text="FAIL"): + click.echo("{} {}".format(text, self.text)) - def ok(self, text='OK'): - click.echo('{} {}'.format(text, self.text)) + def ok(self, text="OK"): + click.echo("{} {}".format(text, self.text)) def __getattr__(self, name): return self @@ -83,7 +83,7 @@ def wrapper(*args, **kwargs): spinner_class = DummySpinner with spinner_class(**spin_kwargs) as spinner: - kwargs['spinner'] = spinner + kwargs["spinner"] = spinner return func(*args, **kwargs) return wrapper diff --git a/riocli/utils/ssh_tunnel.py b/riocli/utils/ssh_tunnel.py index a97995bb..ae4431a0 100644 --- a/riocli/utils/ssh_tunnel.py +++ b/riocli/utils/ssh_tunnel.py @@ -11,18 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import os -import typing from socket import socket, AF_INET, SOCK_STREAM -import click - -from riocli.utils import random_string, run_bash - def get_free_tcp_port(): tcp = socket(AF_INET, SOCK_STREAM) - tcp.bind(('', 0)) + tcp.bind(("", 0)) _, port = tcp.getsockname() tcp.close() return port diff --git a/riocli/utils/state.py b/riocli/utils/state.py index 8da6fc50..66cc7197 100644 --- a/riocli/utils/state.py +++ b/riocli/utils/state.py @@ -11,15 +11,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import os import json +import os from munch import Munch, munchify class StateFile(object): - _MARKERS = ['.git', '.project'] - _STATE_FILE = '.rio' + _MARKERS = [".git", ".project"] + _STATE_FILE = ".rio" def __init__(self, dir_name: str = os.getcwd()): self._dir_name = dir_name @@ -29,7 +29,7 @@ def save(self) -> None: file_path = self._search_state_file() data = json.dumps(self.state) - with open(file_path, 'w') as state_file: + with open(file_path, "w") as state_file: state_file.write(data) def _load_state_file(self) -> Munch: @@ -43,7 +43,6 @@ def _load_state_file(self) -> Munch: state = json.loads(data) return munchify(state) - def _search_state_file(self) -> str: cur_dir = self._dir_name cur_file = self._get_state_file_path(cur_dir) @@ -52,7 +51,7 @@ def _search_state_file(self) -> str: if os.path.exists(cur_file): return cur_file - if cur_dir == '/': + if cur_dir == "/": return self._get_state_file_path(self._dir_name) if self._is_top_dir(cur_dir): @@ -65,12 +64,10 @@ def _search_state_file(self) -> str: cur_dir = os.path.dirname(cur_dir) cur_file = self._get_state_file_path(cur_dir) - def _is_top_dir(self, dir_name: str) -> bool: - if dir == '/': + if dir == "/": return True - for m in self._MARKERS: m_path = os.path.join(dir_name, m) if os.path.exists(m_path): @@ -78,6 +75,5 @@ def _is_top_dir(self, dir_name: str) -> bool: return False - def _get_state_file_path(self, dir_name: str) -> str: return os.path.join(dir_name, self._STATE_FILE) diff --git a/riocli/v2client/__init__.py b/riocli/v2client/__init__.py index 89dd2e23..f7d16b7e 100644 --- a/riocli/v2client/__init__.py +++ b/riocli/v2client/__init__.py @@ -1 +1 @@ -from riocli.v2client.client import Client +from riocli.v2client.client import Client as Client diff --git a/riocli/v2client/client.py b/riocli/v2client/client.py index e29c64cf..8160aab0 100644 --- a/riocli/v2client/client.py +++ b/riocli/v2client/client.py @@ -19,12 +19,10 @@ from hashlib import md5 from typing import Any, Dict, List, Optional -import click import magic from munch import Munch, munchify from rapyuta_io.utils.rest_client import HttpMethod, RestClient -from riocli.constants import Colors from riocli.v2client.enums import DeploymentPhaseConstants, DiskStatusConstants from riocli.v2client.error import DeploymentNotRunning, ImagePullError, RetriesExhausted from riocli.v2client.util import handle_server_errors, process_errors @@ -34,34 +32,35 @@ class Client(object): """ v2 API Client """ + PROD_V2API_URL = "https://api.rapyuta.io" def __init__(self, config, auth_token: str, project: Optional[str] = None): self._config = config - self._host = config.data.get('v2api_host', self.PROD_V2API_URL) + self._host = config.data.get("v2api_host", self.PROD_V2API_URL) self._project = project - self._token = 'Bearer {}'.format(auth_token) + self._token = "Bearer {}".format(auth_token) def _get_auth_header(self: Client, with_project: bool = True) -> dict: headers = dict(Authorization=self._token) - headers['organizationguid'] = self._config.organization_guid + headers["organizationguid"] = self._config.organization_guid if with_project and self._project is not None: - headers['project'] = self._project + headers["project"] = self._project - custom_client_request_id = os.getenv('REQUEST_ID') + custom_client_request_id = os.getenv("REQUEST_ID") if custom_client_request_id: - headers['X-Request-ID'] = custom_client_request_id + headers["X-Request-ID"] = custom_client_request_id return headers # Project APIs def list_projects( - self, - organization_guid: str = None, - query: dict = None, + self, + organization_guid: str = None, + query: dict = None, ) -> Munch: """ List all projects in an organization @@ -73,9 +72,11 @@ def list_projects( params = {} if organization_guid: - params.update({ - "organizations": organization_guid, - }) + params.update( + { + "organizations": organization_guid, + } + ) params.update(query or {}) @@ -88,14 +89,13 @@ def get_project(self, project_guid: str) -> Munch: """ url = "{}/v2/projects/{}/".format(self._host, project_guid) headers = self._get_auth_header() - response = RestClient(url).method( - HttpMethod.GET).headers(headers).execute() + response = RestClient(url).method(HttpMethod.GET).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("projects: {}".format(err_msg)) return munchify(data) @@ -108,17 +108,18 @@ def create_project(self, spec: dict) -> Munch: headers = self._get_auth_header(with_project=False) # Set the organizationguid header - if spec['metadata'].get('organizationGUID'): - headers['organizationguid'] = spec['metadata'].get('organizationGUID') + if spec["metadata"].get("organizationGUID"): + headers["organizationguid"] = spec["metadata"].get("organizationGUID") - response = RestClient(url).method(HttpMethod.POST).headers( - headers).execute(payload=spec) + response = ( + RestClient(url).method(HttpMethod.POST).headers(headers).execute(payload=spec) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("projects: {}".format(err_msg)) return munchify(data) @@ -129,14 +130,15 @@ def update_project(self, project_guid: str, spec: dict) -> Munch: """ url = "{}/v2/projects/{}/".format(self._host, project_guid) headers = self._get_auth_header(with_project=False) - response = RestClient(url).method(HttpMethod.PUT).headers( - headers).execute(payload=spec) + response = ( + RestClient(url).method(HttpMethod.PUT).headers(headers).execute(payload=spec) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("projects: {}".format(err_msg)) return munchify(data) @@ -147,14 +149,18 @@ def update_project_owner(self, project_guid: str, new_owner_guid: str) -> Munch: """ url = "{}/v2/projects/{}/owner/".format(self._host, project_guid) headers = self._get_auth_header(with_project=False) - response = RestClient(url).method(HttpMethod.PUT).headers( - headers).execute(payload={'metadata': {'creatorGUID': new_owner_guid}}) + response = ( + RestClient(url) + .method(HttpMethod.PUT) + .headers(headers) + .execute(payload={"metadata": {"creatorGUID": new_owner_guid}}) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("projects: {}".format(err_msg)) return munchify(data) @@ -165,14 +171,13 @@ def delete_project(self, project_guid: str) -> Munch: """ url = "{}/v2/projects/{}/".format(self._host, project_guid) headers = self._get_auth_header(with_project=False) - response = RestClient(url).method( - HttpMethod.DELETE).headers(headers).execute() + response = RestClient(url).method(HttpMethod.DELETE).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("projects: {}".format(err_msg)) return munchify(data) @@ -184,17 +189,16 @@ def list_providers(self) -> List: """ url = "{}/v2/managedservices/providers/".format(self._host) headers = self._get_auth_header(with_project=False) - response = RestClient(url).method( - HttpMethod.GET).headers(headers).execute() + response = RestClient(url).method(HttpMethod.GET).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("managedservice: {}".format(err_msg)) - return munchify(data.get('items', [])) + return munchify(data.get("items", [])) def list_instances(self) -> List: """ @@ -212,14 +216,13 @@ def get_instance(self, instance_name: str) -> Munch: """ url = "{}/v2/managedservices/{}/".format(self._host, instance_name) headers = self._get_auth_header() - response = RestClient(url).method( - HttpMethod.GET).headers(headers).execute() + response = RestClient(url).method(HttpMethod.GET).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("managedservice: {}".format(err_msg)) return munchify(data) @@ -228,14 +231,18 @@ def create_instance(self, instance: Dict) -> Munch: url = "{}/v2/managedservices/".format(self._host) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.POST).headers( - headers).execute(payload=instance) + response = ( + RestClient(url) + .method(HttpMethod.POST) + .headers(headers) + .execute(payload=instance) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("managedservice: {}".format(err_msg)) return munchify(data) @@ -243,19 +250,18 @@ def create_instance(self, instance: Dict) -> Munch: def delete_instance(self, instance_name) -> Munch: url = "{}/v2/managedservices/{}/".format(self._host, instance_name) headers = self._get_auth_header() - response = RestClient(url).method( - HttpMethod.DELETE).headers(headers).execute() + response = RestClient(url).method(HttpMethod.DELETE).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("managedservice: {}".format(err_msg)) return munchify(data) - def list_instance_bindings(self, instance_name: str, labels: str = '') -> List: + def list_instance_bindings(self, instance_name: str, labels: str = "") -> List: """ List all managedservice instances in a project """ @@ -263,23 +269,26 @@ def list_instance_bindings(self, instance_name: str, labels: str = '') -> List: headers = self._get_auth_header() client = RestClient(url).method(HttpMethod.GET).headers(headers) - return self._walk_pages(client, params={'labelSelector': labels}) + return self._walk_pages(client, params={"labelSelector": labels}) def create_instance_binding(self, instance_name, binding: dict) -> Munch: """ Create a new managed service instance binding """ - url = "{}/v2/managedservices/{}/bindings/".format( - self._host, instance_name) + url = "{}/v2/managedservices/{}/bindings/".format(self._host, instance_name) headers = self._get_auth_header() - response = RestClient(url).method( - HttpMethod.POST).headers(headers).execute(payload=binding) + response = ( + RestClient(url) + .method(HttpMethod.POST) + .headers(headers) + .execute(payload=binding) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("managedservice: {}".format(err_msg)) return munchify(data) @@ -289,16 +298,16 @@ def get_instance_binding(self, instance_name: str, binding_name: str) -> Munch: Get a managed service instance binding """ url = "{}/v2/managedservices/{}/bindings/{}/".format( - self._host, instance_name, binding_name) + self._host, instance_name, binding_name + ) headers = self._get_auth_header() - response = RestClient(url).method( - HttpMethod.GET).headers(headers).execute() + response = RestClient(url).method(HttpMethod.GET).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("managedservice: {}".format(err_msg)) return munchify(data) @@ -308,24 +317,21 @@ def delete_instance_binding(self, instance_name: str, binding_name: str) -> Munc Delete a managed service instance binding """ url = "{}/v2/managedservices/{}/bindings/{}/".format( - self._host, instance_name, binding_name) + self._host, instance_name, binding_name + ) headers = self._get_auth_header() - response = RestClient(url).method( - HttpMethod.DELETE).headers(headers).execute() + response = RestClient(url).method(HttpMethod.DELETE).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("managedservice: {}".format(err_msg)) return munchify(data) - def list_static_routes( - self, - query: dict = None - ) -> Munch: + def list_static_routes(self, query: dict = None) -> Munch: """ List all static routes in a project """ @@ -344,14 +350,13 @@ def get_static_route(self, name: str) -> Munch: """ url = "{}/v2/staticroutes/{}/".format(self._host, name) headers = self._get_auth_header() - response = RestClient(url).method( - HttpMethod.GET).headers(headers).execute() + response = RestClient(url).method(HttpMethod.GET).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("static routes: {}".format(err_msg)) return munchify(data) @@ -362,14 +367,18 @@ def create_static_route(self, metadata: dict) -> Munch: """ url = "{}/v2/staticroutes/".format(self._host) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.POST).headers( - headers).execute(payload=metadata) + response = ( + RestClient(url) + .method(HttpMethod.POST) + .headers(headers) + .execute(payload=metadata) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("static routes: {}".format(err_msg)) return munchify(data) @@ -380,14 +389,15 @@ def update_static_route(self, name: str, sr: dict) -> Munch: """ url = "{}/v2/staticroutes/{}/".format(self._host, name) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.PUT).headers( - headers).execute(payload=sr) + response = ( + RestClient(url).method(HttpMethod.PUT).headers(headers).execute(payload=sr) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("static routes: {}".format(err_msg)) return munchify(data) @@ -398,14 +408,13 @@ def delete_static_route(self, name: str) -> Munch: """ url = "{}/v2/staticroutes/{}/".format(self._host, name) headers = self._get_auth_header() - response = RestClient(url).method( - HttpMethod.DELETE).headers(headers).execute() + response = RestClient(url).method(HttpMethod.DELETE).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("static routes: {}".format(err_msg)) return munchify(data) @@ -416,13 +425,17 @@ def create_secret(self, payload: dict) -> Munch: """ url = "{}/v2/secrets/".format(self._host) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.POST).headers( - headers).execute(payload=payload) + response = ( + RestClient(url) + .method(HttpMethod.POST) + .headers(headers) + .execute(payload=payload) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("secret: {}".format(err_msg)) return munchify(data) @@ -433,21 +446,17 @@ def delete_secret(self, secret_name: str) -> Munch: """ url = "{}/v2/secrets/{}/".format(self._host, secret_name) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.DELETE).headers( - headers).execute() + response = RestClient(url).method(HttpMethod.DELETE).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("secret: {}".format(err_msg)) return munchify(data) - def list_secrets( - self, - query: dict = None - ) -> Munch: + def list_secrets(self, query: dict = None) -> Munch: """ List all secrets in a project """ @@ -460,10 +469,7 @@ def list_secrets( client = RestClient(url).method(HttpMethod.GET).headers(headers) return self._walk_pages(client, params=params) - def get_secret( - self, - secret_name: str - ) -> Munch: + def get_secret(self, secret_name: str) -> Munch: """ Get secret by name """ @@ -473,7 +479,7 @@ def get_secret( response = RestClient(url).method(HttpMethod.GET).headers(headers).execute() data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("secrets: {}".format(err_msg)) return munchify(data) @@ -484,13 +490,14 @@ def update_secret(self, secret_name: str, spec: dict) -> Munch: """ url = "{}/v2/secrets/{}/".format(self._host, secret_name) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.PUT).headers( - headers).execute(payload=spec) + response = ( + RestClient(url).method(HttpMethod.PUT).headers(headers).execute(payload=spec) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("secret: {}".format(err_msg)) return munchify(data) @@ -505,13 +512,17 @@ def list_config_trees(self) -> Munch: def create_config_tree(self, tree_spec: dict) -> Munch: url = "{}/v2/configtrees/".format(self._host) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.POST).headers( - headers).execute(payload=tree_spec) + response = ( + RestClient(url) + .method(HttpMethod.POST) + .headers(headers) + .execute(payload=tree_spec) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("configtree: {}".format(err_msg)) return munchify(data) @@ -519,35 +530,44 @@ def create_config_tree(self, tree_spec: dict) -> Munch: def delete_config_tree(self, tree_name: str) -> Munch: url = "{}/v2/configtrees/{}/".format(self._host, tree_name) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.DELETE).headers( - headers).execute() + response = RestClient(url).method(HttpMethod.DELETE).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("configtree: {}".format(err_msg)) return munchify(data) - def get_config_tree(self, tree_name: str, rev_id: Optional[str] = None, - include_data: bool = False, filter_content_types: Optional[List[str]] = None, - filter_prefixes: Optional[List[str]] = None) -> Munch: + def get_config_tree( + self, + tree_name: str, + rev_id: Optional[str] = None, + include_data: bool = False, + filter_content_types: Optional[List[str]] = None, + filter_prefixes: Optional[List[str]] = None, + ) -> Munch: url = "{}/v2/configtrees/{}/".format(self._host, tree_name) query = { - 'includeData': include_data, - 'contentTypes': filter_content_types, - 'keyPrefixes': filter_prefixes, - 'revision': rev_id, + "includeData": include_data, + "contentTypes": filter_content_types, + "keyPrefixes": filter_prefixes, + "revision": rev_id, } headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.GET).headers( - headers).query_param(query).execute() + response = ( + RestClient(url) + .method(HttpMethod.GET) + .headers(headers) + .query_param(query) + .execute() + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("configtree: {}".format(err_msg)) return munchify(data) @@ -555,47 +575,53 @@ def get_config_tree(self, tree_name: str, rev_id: Optional[str] = None, def set_revision_config_tree(self, tree_name: str, spec: dict) -> Munch: url = "{}/v2/configtrees/{}/".format(self._host, tree_name) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.PUT).headers( - headers).execute(payload=spec) + response = ( + RestClient(url).method(HttpMethod.PUT).headers(headers).execute(payload=spec) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("configtree: {}".format(err_msg)) return munchify(data) - def list_config_tree_revisions(self, tree_name: str, labels: str = '') -> Munch: + def list_config_tree_revisions(self, tree_name: str, labels: str = "") -> Munch: url = "{}/v2/configtrees/{}/revisions/".format(self._host, tree_name) headers = self._get_auth_header() client = RestClient(url).method(HttpMethod.GET).headers(headers) - return self._walk_pages(client, params={'labelSelector': labels}) + return self._walk_pages(client, params={"labelSelector": labels}) def initialize_config_tree_revision(self, tree_name: str) -> Munch: url = "{}/v2/configtrees/{}/revisions/".format(self._host, tree_name) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.POST).headers( - headers).execute() + response = RestClient(url).method(HttpMethod.POST).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("configtree: {}".format(err_msg)) return munchify(data) - def commit_config_tree_revision(self, tree_name: str, rev_id: str, payload: dict) -> Munch: + def commit_config_tree_revision( + self, tree_name: str, rev_id: str, payload: dict + ) -> Munch: url = "{}/v2/configtrees/{}/revisions/{}/".format(self._host, tree_name, rev_id) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.PATCH).headers( - headers).execute(payload=payload) + response = ( + RestClient(url) + .method(HttpMethod.PATCH) + .headers(headers) + .execute(payload=payload) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("configtree: {}".format(err_msg)) return munchify(data) @@ -603,80 +629,100 @@ def commit_config_tree_revision(self, tree_name: str, rev_id: str, payload: dict def store_keys_in_revision(self, tree_name: str, rev_id: str, payload: Any) -> Munch: url = "{}/v2/configtrees/{}/revisions/{}/".format(self._host, tree_name, rev_id) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.PUT).headers( - headers).execute(payload=payload) + response = ( + RestClient(url) + .method(HttpMethod.PUT) + .headers(headers) + .execute(payload=payload) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("configtree: {}".format(err_msg)) return munchify(data) - def store_key_in_revision(self, tree_name: str, rev_id: str, key: str, value: str, perms: int = 644) -> Munch: - url = "{}/v2/configtrees/{}/revisions/{}/{}".format(self._host, tree_name, rev_id, key) + def store_key_in_revision( + self, tree_name: str, rev_id: str, key: str, value: str, perms: int = 644 + ) -> Munch: + url = "{}/v2/configtrees/{}/revisions/{}/{}".format( + self._host, tree_name, rev_id, key + ) headers = self._get_auth_header() - headers['Content-Type'] = 'kv' - headers['X-Checksum'] = md5(str(value).encode('utf-8')).hexdigest() - headers['X-Permissions'] = str(perms) + headers["Content-Type"] = "kv" + headers["X-Checksum"] = md5(str(value).encode("utf-8")).hexdigest() + headers["X-Permissions"] = str(perms) - response = RestClient(url).method(HttpMethod.PUT).headers( - headers).execute(value) + response = RestClient(url).method(HttpMethod.PUT).headers(headers).execute(value) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("configtree: {}".format(err_msg)) return munchify(data) - def store_file_in_revision(self, tree_name: str, rev_id: str, key: str, file_path: str) -> Munch: + def store_file_in_revision( + self, tree_name: str, rev_id: str, key: str, file_path: str + ) -> Munch: stat = os.stat(file_path) perms = oct(stat.st_mode & 0o777)[-3:] content_type = magic.from_file(file_path, mime=True) - url = "{}/v2/configtrees/{}/revisions/{}/{}".format(self._host, tree_name, rev_id, key) + url = "{}/v2/configtrees/{}/revisions/{}/{}".format( + self._host, tree_name, rev_id, key + ) headers = self._get_auth_header() - headers['Content-Type'] = content_type - headers['X-Permissions'] = perms + headers["Content-Type"] = content_type + headers["X-Permissions"] = perms - with open(file_path, 'rb') as f: + with open(file_path, "rb") as f: file_hash = md5() chunk = f.read(8192) while chunk: file_hash.update(chunk) chunk = f.read(8192) - headers['X-Checksum'] = file_hash.hexdigest() + headers["X-Checksum"] = file_hash.hexdigest() f.seek(0) - response = RestClient(url).method(HttpMethod.PUT).headers(headers).execute(payload=f, raw=True) + response = ( + RestClient(url) + .method(HttpMethod.PUT) + .headers(headers) + .execute(payload=f, raw=True) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("configtree: {}".format(err_msg)) return munchify(data) def delete_key_in_revision(self, tree_name: str, rev_id: str, key: str) -> Munch: - url = "{}/v2/configtrees/{}/revisions/{}/{}".format(self._host, tree_name, rev_id, key) + url = "{}/v2/configtrees/{}/revisions/{}/{}".format( + self._host, tree_name, rev_id, key + ) headers = self._get_auth_header() response = RestClient(url).method(HttpMethod.DELETE).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("configtree: {}".format(err_msg)) return munchify(data) - def _walk_pages(self, c: RestClient, params: dict = None, limit: Optional[int] = None) -> Munch: + def _walk_pages( + self, c: RestClient, params: dict = None, limit: Optional[int] = None + ) -> Munch: offset, result = 0, [] params = params or {} @@ -690,22 +736,19 @@ def _walk_pages(self, c: RestClient, params: dict = None, limit: Optional[int] = response = c.query_param(params).execute() data = response.json() if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("listing: {}".format(err_msg)) - items = data.get('items', []) + items = data.get("items", []) if not items: break - offset = data['metadata']['continue'] + offset = data["metadata"]["continue"] result.extend(items) return munchify(result) - def list_packages( - self, - query: dict = None - ) -> Munch: + def list_packages(self, query: dict = None) -> Munch: """ List all packages in a project """ @@ -721,21 +764,25 @@ def create_package(self, payload: dict) -> Munch: """ url = "{}/v2/packages/".format(self._host) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.POST).headers( - headers).execute(payload=payload) + response = ( + RestClient(url) + .method(HttpMethod.POST) + .headers(headers) + .execute(payload=payload) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("package: {}".format(err_msg)) return munchify(data) def get_package( - self, - name: str, - query: dict = None, + self, + name: str, + query: dict = None, ) -> Munch: """ List all packages in a project @@ -746,17 +793,21 @@ def get_package( params = {} params.update(query or {}) - response = RestClient(url).method(HttpMethod.GET).query_param( - params).headers(headers).execute() + response = ( + RestClient(url) + .method(HttpMethod.GET) + .query_param(params) + .headers(headers) + .execute() + ) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("package: {}".format(err_msg)) return munchify(data) - def delete_package(self, package_name: str, - query: dict = None) -> Munch: + def delete_package(self, package_name: str, query: dict = None) -> Munch: """ Delete a secret """ @@ -766,20 +817,25 @@ def delete_package(self, package_name: str, params = {} params.update(query or {}) - response = RestClient(url).method(HttpMethod.DELETE).query_param( - params).headers(headers).execute() + response = ( + RestClient(url) + .method(HttpMethod.DELETE) + .query_param(params) + .headers(headers) + .execute() + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("package: {}".format(err_msg)) return munchify(data) def list_networks( - self, - query: dict = None, + self, + query: dict = None, ) -> Munch: """ List all networks in a project @@ -791,19 +847,26 @@ def list_networks( params.update(query or {}) offset, result = 0, [] while True: - params.update({ - "continue": offset, - }) - response = RestClient(url).method(HttpMethod.GET).query_param( - params).headers(headers).execute() + params.update( + { + "continue": offset, + } + ) + response = ( + RestClient(url) + .method(HttpMethod.GET) + .query_param(params) + .headers(headers) + .execute() + ) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("networks: {}".format(err_msg)) - networks = data.get('items', []) + networks = data.get("items", []) if not networks: break - offset = data['metadata']['continue'] + offset = data["metadata"]["continue"] result.extend(networks) return munchify(result) @@ -814,22 +877,26 @@ def create_network(self, payload: dict) -> Munch: """ url = "{}/v2/networks/".format(self._host) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.POST).headers( - headers).execute(payload=payload) + response = ( + RestClient(url) + .method(HttpMethod.POST) + .headers(headers) + .execute(payload=payload) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("network: {}".format(err_msg)) return munchify(data) def get_network( - self, - name: str, - query: dict = None, + self, + name: str, + query: dict = None, ) -> Munch: """ get a network in a project @@ -840,23 +907,28 @@ def get_network( params = {} params.update(query or {}) - response = RestClient(url).method(HttpMethod.GET).query_param( - params).headers(headers).execute() + response = ( + RestClient(url) + .method(HttpMethod.GET) + .query_param(params) + .headers(headers) + .execute() + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("network: {}".format(err_msg)) return munchify(data) def delete_network( - self, - network_name: str, - query: dict = None, + self, + network_name: str, + query: dict = None, ) -> Munch: """ Delete a secret @@ -867,23 +939,28 @@ def delete_network( params = {} params.update(query or {}) - response = RestClient(url).method(HttpMethod.DELETE).query_param( - params).headers(headers).execute() + response = ( + RestClient(url) + .method(HttpMethod.DELETE) + .query_param(params) + .headers(headers) + .execute() + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("network: {}".format(err_msg)) return munchify(data) def poll_network( - self, - name: str, - retry_count: int = 50, - sleep_interval: int = 6, - ready_phases: List[str] = None, + self, + name: str, + retry_count: int = 50, + sleep_interval: int = 6, + ready_phases: List[str] = None, ) -> Munch: if ready_phases is None: ready_phases = [] @@ -897,27 +974,37 @@ def poll_network( return network if status.phase == DeploymentPhaseConstants.DeploymentPhaseProvisioning.value: - errors = status.get('error_codes', []) - if 'DEP_E153' in errors: # DEP_E153 (image-pull error) will persist across retries - raise ImagePullError('Network not running. Phase: Provisioning Status: {}'.format(status.phase)) + errors = status.get("error_codes", []) + if ( + "DEP_E153" in errors + ): # DEP_E153 (image-pull error) will persist across retries + raise ImagePullError( + "Network not running. Phase: Provisioning Status: {}".format( + status.phase + ) + ) elif status.phase == DeploymentPhaseConstants.DeploymentPhaseSucceeded.value: return network elif status.phase == DeploymentPhaseConstants.DeploymentPhaseStopped.value: - raise DeploymentNotRunning('Network not running. Phase: Stopped Status: {}'.format(status.phase)) + raise DeploymentNotRunning( + "Network not running. Phase: Stopped Status: {}".format(status.phase) + ) time.sleep(sleep_interval) network = self.get_network(name) status = network.status - msg = 'Retries exhausted: Tried {} times with {}s interval. Network: phase={} status={} \n{}'.format( - retry_count, sleep_interval, status.phase, status.status, process_errors(status.get('error_codes', []))) + msg = "Retries exhausted: Tried {} times with {}s interval. Network: phase={} status={} \n{}".format( + retry_count, + sleep_interval, + status.phase, + status.status, + process_errors(status.get("error_codes", [])), + ) raise RetriesExhausted(msg) - def list_deployments( - self, - query: dict = None - ) -> Munch: + def list_deployments(self, query: dict = None) -> Munch: """ List all deployments in a project """ @@ -934,14 +1021,18 @@ def create_deployment(self, deployment: dict) -> Munch: headers = self._get_auth_header() deployment["metadata"]["projectGUID"] = headers["project"] - response = RestClient(url).method(HttpMethod.POST).headers( - headers).execute(payload=deployment) + response = ( + RestClient(url) + .method(HttpMethod.POST) + .headers(headers) + .execute(payload=deployment) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("deployment: {}".format(err_msg)) return munchify(data) @@ -957,7 +1048,7 @@ def get_deployment(self, name: str): data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("deployment: {}".format(err_msg)) return munchify(data) @@ -968,12 +1059,13 @@ def update_deployment(self, name: str, dep: dict) -> Munch: """ url = "{}/v2/deployments/{}/".format(self._host, name) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.PATCH).headers( - headers).execute(payload=dep) + response = ( + RestClient(url).method(HttpMethod.PATCH).headers(headers).execute(payload=dep) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("deployment: {}".format(err_msg)) return munchify(data) @@ -986,22 +1078,21 @@ def delete_deployment(self, name: str, query: dict = None) -> Munch: headers = self._get_auth_header() params = {} params.update(query or {}) - response = RestClient(url).method( - HttpMethod.DELETE).headers(headers).execute() + response = RestClient(url).method(HttpMethod.DELETE).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("deployment: {}".format(err_msg)) return munchify(data) def poll_deployment( - self, - name: str, - retry_count: int = 50, - sleep_interval: int = 6, - ready_phases: List[str] = None, + self, + name: str, + retry_count: int = 50, + sleep_interval: int = 6, + ready_phases: List[str] = None, ) -> Munch: if ready_phases is None: ready_phases = [] @@ -1015,40 +1106,58 @@ def poll_deployment( return deployment if status.phase == DeploymentPhaseConstants.DeploymentPhaseProvisioning.value: - errors = status.get('error_codes', []) - if 'DEP_E153' in errors: # DEP_E153 (image-pull error) will persist across retries - raise ImagePullError('Deployment not running. Phase: Provisioning Status: {}'.format(status.phase)) + errors = status.get("error_codes", []) + if ( + "DEP_E153" in errors + ): # DEP_E153 (image-pull error) will persist across retries + raise ImagePullError( + "Deployment not running. Phase: Provisioning Status: {}".format( + status.phase + ) + ) elif status.phase == DeploymentPhaseConstants.DeploymentPhaseSucceeded.value: return deployment elif status.phase == DeploymentPhaseConstants.DeploymentPhaseStopped.value: - raise DeploymentNotRunning('Deployment not running. Phase: Stopped Status: {}'.format(status.phase)) + raise DeploymentNotRunning( + "Deployment not running. Phase: Stopped Status: {}".format( + status.phase + ) + ) time.sleep(sleep_interval) deployment = self.get_deployment(name) status = deployment.status - msg = 'Retries exhausted: Tried {} times with {}s interval. Deployment: phase={} status={} \n{}'.format( - retry_count, sleep_interval, status.phase, status.status, process_errors(status.get('error_codes', []))) + msg = "Retries exhausted: Tried {} times with {}s interval. Deployment: phase={} status={} \n{}".format( + retry_count, + sleep_interval, + status.phase, + status.status, + process_errors(status.get("error_codes", [])), + ) raise RetriesExhausted(msg) def stream_deployment_logs( - self, - name: str, - executable: str, - replica: int = 0, + self, + name: str, + executable: str, + replica: int = 0, ): - url = "{}/v2/deployments/{}/logs/?replica={}&executable={}".format(self._host, name, replica, executable) + url = "{}/v2/deployments/{}/logs/?replica={}&executable={}".format( + self._host, name, replica, executable + ) headers = self._get_auth_header() curl = 'curl -H "project: {}" -H "Authorization: {}" "{}"'.format( - headers['project'], headers['Authorization'], url) + headers["project"], headers["Authorization"], url + ) os.system(curl) def list_disks( - self, - query: dict = None, + self, + query: dict = None, ) -> Munch: """ List all disks in a project @@ -1065,14 +1174,13 @@ def get_disk(self, name: str) -> Munch: """ url = "{}/v2/disks/{}/".format(self._host, name) headers = self._get_auth_header() - response = RestClient(url).method( - HttpMethod.GET).headers(headers).execute() + response = RestClient(url).method(HttpMethod.GET).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("disks: {}".format(err_msg)) return munchify(data) @@ -1083,14 +1191,15 @@ def create_disk(self, disk: dict) -> Munch: """ url = "{}/v2/disks/".format(self._host) headers = self._get_auth_header() - response = RestClient(url).method(HttpMethod.POST).headers( - headers).execute(payload=disk) + response = ( + RestClient(url).method(HttpMethod.POST).headers(headers).execute(payload=disk) + ) handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("disks: {}".format(err_msg)) return munchify(data) @@ -1101,38 +1210,44 @@ def delete_disk(self, name: str) -> Munch: """ url = "{}/v2/disks/{}/".format(self._host, name) headers = self._get_auth_header() - response = RestClient(url).method( - HttpMethod.DELETE).headers(headers).execute() + response = RestClient(url).method(HttpMethod.DELETE).headers(headers).execute() handle_server_errors(response) data = json.loads(response.text) if not response.ok: - err_msg = data.get('error') + err_msg = data.get("error") raise Exception("disks: {}".format(err_msg)) return munchify(data) def poll_disk( - self, - name: str, - retry_count: int = 50, - sleep_interval: int = 6, + self, + name: str, + retry_count: int = 50, + sleep_interval: int = 6, ) -> Munch: disk = self.get_disk(name) status = disk.status for _ in range(retry_count): - if status.status in [DiskStatusConstants.DiskStatusAvailable.value, - DiskStatusConstants.DiskStatusReleased.value]: + if status.status in [ + DiskStatusConstants.DiskStatusAvailable.value, + DiskStatusConstants.DiskStatusReleased.value, + ]: return disk elif status.status == DiskStatusConstants.DiskStatusFailed.value: - raise DeploymentNotRunning('Disk not running. Status: {}'.format(status.status)) + raise DeploymentNotRunning( + "Disk not running. Status: {}".format(status.status) + ) time.sleep(sleep_interval) disk = self.get_disk(name) status = disk.status - raise RetriesExhausted('Retries exhausted: Tried {} times with {}s interval. Disk: status={}'.format( - retry_count, sleep_interval, status.status)) + raise RetriesExhausted( + "Retries exhausted: Tried {} times with {}s interval. Disk: status={}".format( + retry_count, sleep_interval, status.status + ) + ) diff --git a/riocli/v2client/constants.py b/riocli/v2client/constants.py index 94321788..6bb899ec 100644 --- a/riocli/v2client/constants.py +++ b/riocli/v2client/constants.py @@ -16,225 +16,243 @@ # Device related Error codes "DEP_E151": { "description": "Device is either offline or not reachable", - "action": "Ensure that the device is connected to the internet" + "action": "Ensure that the device is connected to the internet", }, "DEP_E302": { "description": "Supervisord related error on device", - "action": ("Ensure supervisord is installed on rapyuta virtual environment and working\n" - "Re-onboard the device, if issue persists report to rapyuta.io support.") + "action": ( + "Ensure supervisord is installed on rapyuta virtual environment and working\n" + "Re-onboard the device, if issue persists report to rapyuta.io support." + ), }, "DEP_E161": { "description": "Docker image not found for executables of components deployed on device", - "action": "Ensure that the docker image path is valid and secrets have privileges" + "action": "Ensure that the docker image path is valid and secrets have privileges", }, "DEP_E316": { "description": "Docker image pull failed in device", - "action": "Ensure that the docker image path is valid and secrets have privileges" + "action": "Ensure that the docker image path is valid and secrets have privileges", }, "DEP_E317": { "description": "Missing subpath or empty mount path", - "action": ("Ensure the current device has a subpath\n" - "Ensure that the mount path isn’t empty") + "action": ( + "Ensure the current device has a subpath\n" + "Ensure that the mount path isn’t empty" + ), }, "DEP_E304": { "description": "DeviceManager internal error", - "action": "Report to rapyuta.io support" + "action": "Report to rapyuta.io support", }, "DEP_E306": { "description": "DeviceEdge internal error", - "action": "Ensure deviceedge is up and running" + "action": "Ensure deviceedge is up and running", }, "DEP_E307": { "description": "DeviceEdge bad request", - "action": "Report to rapyuta.io support" + "action": "Report to rapyuta.io support", }, - # Application related errors. "DEP_E152": { "description": "Executables of the component deployed on the device either exited too early or failed", - "action": "Executables of the component deployed on the device either exited too early or failed" + "action": "Executables of the component deployed on the device either exited too early or failed", }, "DEP_E153": { "description": "Unable to pull the docker image for the component deployed on cloud", - "action": ("Ensure that the docker image provided while adding the package still " - "exists at the specified registry endpoint ") + "action": ( + "Ensure that the docker image provided while adding the package still " + "exists at the specified registry endpoint " + ), }, "DEP_E154": { "description": "Executables of the component deployed on cloud exited too early", - "action": "Troubleshoot the failed component by analyzing the deployment logs" + "action": "Troubleshoot the failed component by analyzing the deployment logs", }, "DEP_E155": { "description": "Executables of the component deployed on cloud failed", - "action": "Troubleshoot the failed component by analyzing the deployment logs" + "action": "Troubleshoot the failed component by analyzing the deployment logs", }, "DEP_E156": { "description": "Dependent deployment is in error state", - "action": "Troubleshoot the dependent deployment that is in error state" + "action": "Troubleshoot the dependent deployment that is in error state", }, "DEP_E162": { - "description": ("Validation error. Cases include:\nInconsistent values of ROS distro " - "and CPU architecture variables for the device and package being provisioned. " - "\nrapyuta.io docker images not present on docker device."), - "action": ("Create package with appropriate values for ROS distro and CPU architecture " - "variables.\nOnboard the device again.") + "description": ( + "Validation error. Cases include:\nInconsistent values of ROS distro " + "and CPU architecture variables for the device and package being provisioned. " + "\nrapyuta.io docker images not present on docker device." + ), + "action": ( + "Create package with appropriate values for ROS distro and CPU architecture " + "variables.\nOnboard the device again." + ), }, "DEP_E163": { "description": "Application has stopped and exited unexpectedly, and crashes continuously on device", - "action": "Debug the application using the corresponding deployment logs" + "action": "Debug the application using the corresponding deployment logs", }, - # CloudBridge related error codes. "DEP_E171": { "description": "Cloud bridge encountered duplicate alias on the device", - "action": ("Change the alias name during deployment and ensure that there" - " is no duplication of alias name under the same routed network") + "action": ( + "Change the alias name during deployment and ensure that there" + " is no duplication of alias name under the same routed network" + ), }, "DEP_E172": { "description": "Compression library required for the cloud bridge is missing on the device", - "action": "Re-onboard the device" + "action": "Re-onboard the device", }, "DEP_E173": { "description": "Transport libraries required for the cloud bridge are missing on the device", - "action": "Re-onboard the device" + "action": "Re-onboard the device", }, "DEP_E174": { "description": "Cloud bridge on the device encountered multiple ROS service origins", - "action": ("Ensure that there aren’t multiple deployments with the same ROS service " - "endpoint under the same routed network") + "action": ( + "Ensure that there aren’t multiple deployments with the same ROS service " + "endpoint under the same routed network" + ), }, "DEP_E175": { "description": "Python actionlib/msgs required for the cloud bridge is missing on the device", - "action": "Re-onboard the device" + "action": "Re-onboard the device", }, "DEP_E176": { "description": "Cloud bridge encountered duplicate alias on the cloud component", - "action": ("Change the alias name during deployment and ensure that there is no" - " duplication of alias name under the same routed network") + "action": ( + "Change the alias name during deployment and ensure that there is no" + " duplication of alias name under the same routed network" + ), }, "DEP_E177": { "description": "Cloud bridge on the cloud component encountered multiple ROS service origins", - "action": "Re-onboard the device" + "action": "Re-onboard the device", }, - # Docker image related Error codes "DEP_E350": { "description": "Get http error when pulling image from registry on device", - "action": "Ensure registry is accesible." + "action": "Ensure registry is accesible.", }, "DEP_E351": { "description": "Unable to parse the image name on device", - "action": "Ensure image name is correct and available in registry." + "action": "Ensure image name is correct and available in registry.", }, "DEP_E352": { "description": "Unable to inspect image on device", - "action": "Ensure valid image is present" + "action": "Ensure valid image is present", }, "DEP_E353": { "description": "Required Image is absent on device and PullPolicy is NeverPullImage", - "action": "Ensure pull policy is not set to never or image is present on device" + "action": "Ensure pull policy is not set to never or image is present on device", }, - # Container related error codes "DEP_E360": { "description": "Failed to create container config on device", - "action": ("Ensure executable config is mounted correctly\n" - "If issue persists report to rapyuta.io support") + "action": ( + "Ensure executable config is mounted correctly\n" + "If issue persists report to rapyuta.io support" + ), }, "DEP_E361": { "description": "Runtime failed to start any of pod's container on device", - "action": ("Ensure executable command is valid\n" - "If issue persists report to rapyuta.io support") + "action": ( + "Ensure executable command is valid\n" + "If issue persists report to rapyuta.io support" + ), }, "DEP_E362": { "description": "Runtime failed to create container on device", - "action": ("Report to rapyuta.io support") + "action": ("Report to rapyuta.io support"), }, "DEP_E363": { "description": "Runtime failed to kill any of pod's containers on device", - "action": ("Report to rapyuta.io support") + "action": ("Report to rapyuta.io support"), }, "DEP_E364": { "description": "Runtime failed to create a sandbox for pod on device", - "action": ("Report to rapyuta.io support") + "action": ("Report to rapyuta.io support"), }, "DEP_E365": { "description": "Runtime failed to get pod sandbox config from pod on device", - "action": ("Report to rapyuta.io support") + "action": ("Report to rapyuta.io support"), }, "DEP_E366": { "description": "Runtime failed to stop pod's sandbox on device", - "action": ("Report to rapyuta.io support") + "action": ("Report to rapyuta.io support"), }, "DEP_E399": { "description": "Deployment failed for some unknown reason on device", - "action": ("Report to rapyuta.io support") + "action": ("Report to rapyuta.io support"), }, - # ROS Comm Related Device Errors "DEP_E303": { "description": "Cloud Bridge executable not running on device", - "action": ("Troubleshoot Cloud Bridge container on device by analyzing logs") + "action": ("Troubleshoot Cloud Bridge container on device by analyzing logs"), }, "DEP_E305": { "description": "Native Network executable not running on device", - "action": ("Troubleshoot native network container on device by analyzing logs") + "action": ("Troubleshoot native network container on device by analyzing logs"), }, "DEP_E313": { "description": "ROS Master executable not running on device", - "action": ("Troubleshoot ROS Master container on device by analyzing logs") + "action": ("Troubleshoot ROS Master container on device by analyzing logs"), }, "DEP_E330": { "description": "ROS2 Native Network executable not running on device", - "action": ("Troubleshoot ROS2 Routed Network container on device by analyzing logs") + "action": ( + "Troubleshoot ROS2 Routed Network container on device by analyzing logs" + ), }, "DEP_E331": { "description": "ROS2 Routed Network executable not running on device", - "action": ("Troubleshoot ROS2 Routed Network container on device by analyzing logs") + "action": ( + "Troubleshoot ROS2 Routed Network container on device by analyzing logs" + ), }, - # Cloud error codes "DEP_E201": { "description": "Cloud component deployment pending", - "action": ("Report to rapyuta.io support") + "action": ("Report to rapyuta.io support"), }, "DEP_E202": { "description": "Cloud component status unknown", - "action": ("Report to rapyuta.io support") + "action": ("Report to rapyuta.io support"), }, "DEP_E203": { "description": "Cloud Bridge not running on cloud", - "action": "Troubleshoot the failed component by analyzing the deployment logs" + "action": "Troubleshoot the failed component by analyzing the deployment logs", }, "DEP_E204": { "description": "ROS Master not running on cloud", - "action": "Troubleshoot the failed component by analyzing the deployment logs" + "action": "Troubleshoot the failed component by analyzing the deployment logs", }, "DEP_E205": { "description": "Cloud Broker not running on cloud", - "action": "Troubleshoot the failed component by analyzing the deployment logs" + "action": "Troubleshoot the failed component by analyzing the deployment logs", }, "DEP_E208": { "description": "Desired set of replicas not running on cloud", - "action": "Troubleshoot the failed component by analyzing the deployment logs" + "action": "Troubleshoot the failed component by analyzing the deployment logs", }, "DEP_E209": { "description": "Native Network not running on cloud", - "action": "Troubleshoot the failed component by analyzing the deployment logs" + "action": "Troubleshoot the failed component by analyzing the deployment logs", }, "DEP_E210": { "description": "Disk Not Running", - "action": ("Ensure disk is running") + "action": ("Ensure disk is running"), }, "DEP_E213": { "description": "Broker running low on memory", - "action": ("Report to rapyuta.io support") + "action": ("Report to rapyuta.io support"), }, "DEP_E214": { "description": "ROS2 Native Network not running on cloud", - "action": "Troubleshoot the failed component by analyzing the deployment logs" + "action": "Troubleshoot the failed component by analyzing the deployment logs", }, "DEP_E215": { "description": "ROS2 Routed Network not running on cloud", - "action": "Troubleshoot the failed component by analyzing the deployment logs" - } + "action": "Troubleshoot the failed component by analyzing the deployment logs", + }, } diff --git a/riocli/v2client/error.py b/riocli/v2client/error.py index b8724c19..82496b30 100644 --- a/riocli/v2client/error.py +++ b/riocli/v2client/error.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. + class RetriesExhausted(Exception): def __init__(self, msg=None): Exception.__init__(self, msg) @@ -28,13 +29,12 @@ def __init__(self, msg=None): class HttpAlreadyExistsError(Exception): - def __init__(self, message='resource already exists'): + def __init__(self, message="resource already exists"): self.message = message super().__init__(self.message) class HttpNotFoundError(Exception): - def __init__(self, message='resource not found'): + def __init__(self, message="resource not found"): self.message = message super().__init__(self.message) - diff --git a/riocli/v2client/util.py b/riocli/v2client/util.py index cf30c881..dc5910ec 100644 --- a/riocli/v2client/util.py +++ b/riocli/v2client/util.py @@ -26,24 +26,27 @@ def process_errors(errors: typing.List[str], no_action: bool = False) -> str: """Process the deployment errors and return the formatted error message""" - err_fmt = '[{}] {}\nAction: {}' - support_action = ('Report the issue together with the relevant' - ' details to the support team') + err_fmt = "[{}] {}\nAction: {}" + support_action = ( + "Report the issue together with the relevant" " details to the support team" + ) - action, description = '', '' + action, description = "", "" msgs = [] for code in errors: if code in ERRORS: - description = ERRORS[code]['description'] - action = ERRORS[code]['action'] - elif code.startswith('DEP_E2'): - description = 'Internal rapyuta.io error in the components deployed on cloud' + description = ERRORS[code]["description"] + action = ERRORS[code]["action"] + elif code.startswith("DEP_E2"): + description = "Internal rapyuta.io error in the components deployed on cloud" action = support_action - elif code.startswith('DEP_E3'): - description = 'Internal rapyuta.io error in the components deployed on a device' + elif code.startswith("DEP_E3"): + description = ( + "Internal rapyuta.io error in the components deployed on a device" + ) action = support_action - elif code.startswith('DEP_E4'): - description = 'Internal rapyuta.io error' + elif code.startswith("DEP_E4"): + description = "Internal rapyuta.io error" action = support_action code = click.style(code, fg=Colors.YELLOW) @@ -51,11 +54,11 @@ def process_errors(errors: typing.List[str], no_action: bool = False) -> str: action = click.style(action, fg=Colors.GREEN) if no_action: - msgs.append(f'[{code}]: {description}') + msgs.append(f"[{code}]: {description}") else: msgs.append(err_fmt.format(code, description, action)) - return '\n'.join(msgs) + return "\n".join(msgs) def handle_server_errors(response: requests.Response): @@ -64,9 +67,9 @@ def handle_server_errors(response: requests.Response): if status_code < 400: return - err = '' + err = "" try: - err = response.json().get('error') + err = response.json().get("error") except json.JSONDecodeError: err = response.text @@ -78,19 +81,19 @@ def handle_server_errors(response: requests.Response): raise HttpAlreadyExistsError() # 500 Internal Server Error if status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR: - raise Exception('internal server error') + raise Exception("internal server error") # 501 Not Implemented if status_code == http.HTTPStatus.NOT_IMPLEMENTED: - raise Exception('not implemented') + raise Exception("not implemented") # 502 Bad Gateway if status_code == http.HTTPStatus.BAD_GATEWAY: - raise Exception('bad gateway') + raise Exception("bad gateway") # 503 Service Unavailable if status_code == http.HTTPStatus.SERVICE_UNAVAILABLE: - raise Exception('service unavailable') + raise Exception("service unavailable") # 504 Gateway Timeout if status_code == http.HTTPStatus.GATEWAY_TIMEOUT: - raise Exception('gateway timeout') + raise Exception("gateway timeout") # Anything else that is not known if status_code > 504: - raise Exception('unknown server error') + raise Exception("unknown server error") diff --git a/riocli/vpn/__init__.py b/riocli/vpn/__init__.py index ab63af2a..dae6b706 100644 --- a/riocli/vpn/__init__.py +++ b/riocli/vpn/__init__.py @@ -24,8 +24,8 @@ @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color="yellow", + help_options_color="green", ) def vpn() -> None: """Connect to the rapyuta.io VPN diff --git a/riocli/vpn/connect.py b/riocli/vpn/connect.py index 63c5473d..3b686c3a 100644 --- a/riocli/vpn/connect.py +++ b/riocli/vpn/connect.py @@ -31,23 +31,30 @@ stop_tailscale, install_vpn_tools, is_vpn_enabled_in_project, - update_hosts_file + update_hosts_file, ) -_TAILSCALE_CMD_FORMAT = 'tailscale up --auth-key={} --login-server={} --reset --force-reauth ' \ - '--accept-routes --accept-dns --advertise-tags={} --timeout=30s' +_TAILSCALE_CMD_FORMAT = ( + "tailscale up --auth-key={} --login-server={} --reset --force-reauth " + "--accept-routes --accept-dns --advertise-tags={} --timeout=30s" +) @click.command( - 'connect', + "connect", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--update-hosts', 'update_hosts', is_flag=True, - default=False, help='Update hosts file with VPN peers to allow ' - 'access to them by hostname. Run with `sudo` ' - 'when you enable this flag.') +@click.option( + "--update-hosts", + "update_hosts", + is_flag=True, + default=False, + help="Update hosts file with VPN peers to allow " + "access to them by hostname. Run with `sudo` " + "when you enable this flag.", +) @click.pass_context @with_spinner(text="Connecting...") def connect(ctx: click.Context, update_hosts: bool, spinner: Yaspin): @@ -76,51 +83,68 @@ def connect(ctx: click.Context, update_hosts: bool, spinner: Yaspin): if not is_vpn_enabled_in_project(client, config.project_guid): spinner.write( - click.style('{} VPN is not enabled in the project. ' - 'Please ask the organization or project ' - 'creator to enable VPN'.format(Symbols.WAITING), - fg=Colors.YELLOW)) + click.style( + "{} VPN is not enabled in the project. " + "Please ask the organization or project " + "creator to enable VPN".format(Symbols.WAITING), + fg=Colors.YELLOW, + ) + ) raise SystemExit(1) with spinner.hidden(): if is_tailscale_up(): click.confirm( - '{} The VPN client is already running. ' - 'Do you want to stop it and connect to the VPN of ' - 'the current project?'.format(Symbols.WARNING), - default=False, abort=True) + "{} The VPN client is already running. " + "Do you want to stop it and connect to the VPN of " + "the current project?".format(Symbols.WARNING), + default=False, + abort=True, + ) success = stop_tailscale() if not success: msg = ( - '{} Failed to stop tailscale. Please run the ' - 'following commands manually\n sudo tailscale down\n ' - 'sudo tailscale logout'.format(Symbols.ERROR)) + "{} Failed to stop tailscale. Please run the " + "following commands manually\n sudo tailscale down\n " + "sudo tailscale logout".format(Symbols.ERROR) + ) click.secho(msg, fg=Colors.YELLOW) raise SystemExit(1) spinner.write( click.style( - '{} VPN is enabled in the project ({})'.format( - Symbols.INFO, ctx.obj.data.get('project_name')), - fg=Colors.CYAN)) + "{} VPN is enabled in the project ({})".format( + Symbols.INFO, ctx.obj.data.get("project_name") + ), + fg=Colors.CYAN, + ) + ) if not start_tailscale(ctx, spinner): - click.secho('{} Failed to connect to the project VPN'.format( - Symbols.ERROR), fg=Colors.RED) + click.secho( + "{} Failed to connect to the project VPN".format(Symbols.ERROR), + fg=Colors.RED, + ) raise SystemExit(1) if update_hosts and is_tailscale_up(): - spinner.text = 'Updating hosts file...' + spinner.text = "Updating hosts file..." try: update_hosts_file() - spinner.write(click.style(f'{Symbols.SUCCESS} Hosts file updated', fg=Colors.CYAN)) + spinner.write( + click.style(f"{Symbols.SUCCESS} Hosts file updated", fg=Colors.CYAN) + ) except Exception as e: - spinner.write(click.style(f'Failed to update hosts: {str(e)}', fg=Colors.RED)) + spinner.write( + click.style(f"Failed to update hosts: {str(e)}", fg=Colors.RED) + ) - spinner.text = click.style('You are now connected to the project\'s VPN', fg=Colors.GREEN) + spinner.text = click.style( + "You are now connected to the project's VPN", fg=Colors.GREEN + ) spinner.green.ok(Symbols.SUCCESS) except click.exceptions.Abort as e: - spinner.red.text = 'Aborted!' + spinner.red.text = "Aborted!" spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e except Exception as e: @@ -130,10 +154,14 @@ def connect(ctx: click.Context, update_hosts: bool, spinner: Yaspin): def start_tailscale(ctx: click.Context, spinner: Yaspin) -> bool: - spinner.text = 'Generating a token to join the network...' - - binding = create_binding(ctx, delta=timedelta(minutes=10), labels=get_binding_labels()) - cmd = _TAILSCALE_CMD_FORMAT.format(binding.HEADSCALE_PRE_AUTH_KEY, binding.HEADSCALE_URL, binding.HEADSCALE_ACL_TAG) + spinner.text = "Generating a token to join the network..." + + binding = create_binding( + ctx, delta=timedelta(minutes=10), labels=get_binding_labels() + ) + cmd = _TAILSCALE_CMD_FORMAT.format( + binding.HEADSCALE_PRE_AUTH_KEY, binding.HEADSCALE_URL, binding.HEADSCALE_ACL_TAG + ) cmd = priviledged_command(cmd) with spinner.hidden(): @@ -141,8 +169,10 @@ def start_tailscale(ctx: click.Context, spinner: Yaspin) -> bool: if code != 0: spinner.write( - click.style('{} Failed to start vpn client'.format(Symbols.ERROR), - fg=Colors.RED)) + click.style( + "{} Failed to start vpn client".format(Symbols.ERROR), fg=Colors.RED + ) + ) return False return True diff --git a/riocli/vpn/disconnect.py b/riocli/vpn/disconnect.py index 187e9a7e..e6f70934 100644 --- a/riocli/vpn/disconnect.py +++ b/riocli/vpn/disconnect.py @@ -25,7 +25,7 @@ @click.command( - 'disconnect', + "disconnect", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, @@ -46,23 +46,26 @@ def disconnect(ctx: click.Context): if is_tailscale_up() and not stop_tailscale(): click.secho( - '{} Failed to disconnect from VPN. ' - 'Although, trying again may work.'.format(Symbols.ERROR), - fg=Colors.RED) + "{} Failed to disconnect from VPN. " + "Although, trying again may work.".format(Symbols.ERROR), + fg=Colors.RED, + ) raise SystemExit(1) try: cleanup_hosts_file() except Exception as e: - click.secho(f'{Symbols.WARNING} Could not clean ' - f'up hosts file: {str(e)}', fg=Colors.YELLOW) + click.secho( + f"{Symbols.WARNING} Could not clean " f"up hosts file: {str(e)}", + fg=Colors.YELLOW, + ) click.secho( - '{} You have been disconnected from the project\'s VPN'.format( - Symbols.SUCCESS), - fg=Colors.GREEN) + "{} You have been disconnected from the project's VPN".format( + Symbols.SUCCESS + ), + fg=Colors.GREEN, + ) except Exception as e: click.secho(str(e), fg=Colors.RED) raise SystemExit(1) from e - - diff --git a/riocli/vpn/machines.py b/riocli/vpn/machines.py index c04d8d2c..aaf55f16 100644 --- a/riocli/vpn/machines.py +++ b/riocli/vpn/machines.py @@ -28,8 +28,8 @@ @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, ) def machines() -> None: """ @@ -39,7 +39,7 @@ def machines() -> None: @click.command( - 'list', + "list", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, @@ -50,11 +50,11 @@ def list_machines() -> None: This command lists all the machines that are registered on the VPN using the CLI. """ - labels = 'machine-key=true' + labels = "machine-key=true" try: client = new_v2_client() - machines = client.list_instance_bindings('rio-internal-headscale', labels=labels) + machines = client.list_instance_bindings("rio-internal-headscale", labels=labels) display_machines(machines=machines) except Exception as e: click.secho(str(e), fg=Colors.RED) @@ -62,16 +62,18 @@ def list_machines() -> None: @click.command( - 'register', + "register", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('name', type=str) -@click.argument('node_key', type=str) +@click.argument("name", type=str) +@click.argument("node_key", type=str) @click.pass_context @with_spinner(text="Registering machine...") -def register_machine(ctx: click.Context, name: str, node_key: str, spinner: Yaspin) -> None: +def register_machine( + ctx: click.Context, name: str, node_key: str, spinner: Yaspin +) -> None: """Register an Android or iOS Tailscale Client in the project's VPN. Provide a name and the node key of the machine to register it @@ -80,39 +82,48 @@ def register_machine(ctx: click.Context, name: str, node_key: str, spinner: Yasp name that you want to give to the machine. """ labels = get_binding_labels() - labels['machine-key'] = 'true' + labels["machine-key"] = "true" node_key = sanitize_node_key(node_key) try: - create_binding(ctx, name=name, machine=node_key, ephemeral=False, throwaway=False, labels=labels) - spinner.text = click.style('Machine {} registered successfully.'.format(name), fg=Colors.GREEN) + create_binding( + ctx, + name=name, + machine=node_key, + ephemeral=False, + throwaway=False, + labels=labels, + ) + spinner.text = click.style( + "Machine {} registered successfully.".format(name), fg=Colors.GREEN + ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style( - 'Failed to register: {}'.format(e), Colors.RED) + spinner.text = click.style("Failed to register: {}".format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @click.command( - 'deregister', + "deregister", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.argument('name', type=str) +@click.argument("name", type=str) @with_spinner(text="De-registering machine...") def deregister_machine(name: str, spinner: Yaspin) -> None: """Deregister an Android or iOS Tailscale Client in the Project VPN.""" try: client = new_v2_client() client.delete_instance_binding("rio-internal-headscale", name) - spinner.text = click.style('Machine {} de-registered successfully.'.format(name), fg=Colors.GREEN) + spinner.text = click.style( + "Machine {} de-registered successfully.".format(name), fg=Colors.GREEN + ) spinner.green.ok(Symbols.SUCCESS) except Exception as e: - spinner.text = click.style( - 'Failed to de-register: {}'.format(e), Colors.RED) + spinner.text = click.style("Failed to de-register: {}".format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) raise SystemExit(1) from e @@ -120,7 +131,7 @@ def deregister_machine(name: str, spinner: Yaspin) -> None: def display_machines(machines: Iterable, show_header: bool = True) -> None: headers = [] if show_header: - headers = ['Machine Name'] + headers = ["Machine Name"] data = [] for machine in machines: @@ -130,10 +141,10 @@ def display_machines(machines: Iterable, show_header: bool = True) -> None: def sanitize_node_key(node_key: str) -> str: - if node_key.startswith('nodekey:'): + if node_key.startswith("nodekey:"): return node_key - return 'nodekey:{}'.format(node_key) + return "nodekey:{}".format(node_key) machines.add_command(register_machine) diff --git a/riocli/vpn/ping.py b/riocli/vpn/ping.py index 147d4411..2ae3d2dd 100644 --- a/riocli/vpn/ping.py +++ b/riocli/vpn/ping.py @@ -18,11 +18,16 @@ from riocli.constants import Colors, Symbols from riocli.utils.spinner import with_spinner -from riocli.vpn.util import (get_tailscale_status, install_vpn_tools, is_tailscale_up, tailscale_ping) +from riocli.vpn.util import ( + get_tailscale_status, + install_vpn_tools, + is_tailscale_up, + tailscale_ping, +) @click.command( - 'ping-all', + "ping-all", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, @@ -43,13 +48,14 @@ def ping_all(ctx: click.Context, spinner: Yaspin = None): if not is_tailscale_up(): spinner.text = click.style( - 'You are not connected to the VPN', fg=Colors.YELLOW) + "You are not connected to the VPN", fg=Colors.YELLOW + ) spinner.yellow.ok(Symbols.WARNING) return ping_all_peers(spinner) - spinner.text = click.style('Ping complete', fg=Colors.GREEN) + spinner.text = click.style("Ping complete", fg=Colors.GREEN) spinner.green.ok(Symbols.SUCCESS) except Exception as e: spinner.text = click.style(str(e), fg=Colors.RED) @@ -60,17 +66,17 @@ def ping_all(ctx: click.Context, spinner: Yaspin = None): def ping_all_peers(spinner: Yaspin): s = get_tailscale_status() - peers = s.get('Peer', {}) + peers = s.get("Peer", {}) for _, v in peers.items(): # Do not waste time pinging # offline nodes - if not v.get('Online'): + if not v.get("Online"): continue - spinner.text = 'Pinging: {}...'.format( - click.style(v.get('HostName'), italic=True) + spinner.text = "Pinging: {}...".format( + click.style(v.get("HostName"), italic=True) ) - ips = v.get('TailscaleIPs') + ips = v.get("TailscaleIPs") for ip in ips: tailscale_ping(ip) diff --git a/riocli/vpn/status.py b/riocli/vpn/status.py index 392a7850..58fa655d 100644 --- a/riocli/vpn/status.py +++ b/riocli/vpn/status.py @@ -18,17 +18,23 @@ from riocli.config import new_v2_client from riocli.constants import Colors, Symbols from riocli.utils import tabulate_data -from riocli.vpn.util import (get_tailscale_status, install_vpn_tools, is_tailscale_up, is_vpn_enabled_in_project) +from riocli.vpn.util import ( + get_tailscale_status, + install_vpn_tools, + is_tailscale_up, + is_vpn_enabled_in_project, +) @click.command( - 'status', + "status", cls=HelpColorsCommand, help_headers_color=Colors.YELLOW, help_options_color=Colors.GREEN, ) -@click.option('--wide', '-w', is_flag=True, default=False, - help='Print more details', type=bool) +@click.option( + "--wide", "-w", is_flag=True, default=False, help="Print more details", type=bool +) @click.pass_context def status(ctx: click.Context, wide: bool = False): """Check VPN network status. @@ -42,30 +48,35 @@ def status(ctx: click.Context, wide: bool = False): client = new_v2_client() - if not is_vpn_enabled_in_project( - client, ctx.obj.data.get('project_id')): - click.secho('{} VPN is not enabled in the project. ' - 'Please ask the organization or project ' - 'creator to enable VPN'.format(Symbols.WARNING), - fg=Colors.YELLOW) + if not is_vpn_enabled_in_project(client, ctx.obj.data.get("project_id")): + click.secho( + "{} VPN is not enabled in the project. " + "Please ask the organization or project " + "creator to enable VPN".format(Symbols.WARNING), + fg=Colors.YELLOW, + ) raise SystemExit(1) click.secho( - '{} VPN is enabled in the project ({})'.format( - Symbols.INFO, ctx.obj.data.get('project_name')), - fg=Colors.CYAN) + "{} VPN is enabled in the project ({})".format( + Symbols.INFO, ctx.obj.data.get("project_name") + ), + fg=Colors.CYAN, + ) click.echo() if not is_tailscale_up(): click.secho( - '{} You are not connected to the VPN'.format(Symbols.WARNING), - fg=Colors.YELLOW) + "{} You are not connected to the VPN".format(Symbols.WARNING), + fg=Colors.YELLOW, + ) return display_vpn_status(wide) - click.secho('{} You are connected to the VPN.'.format(Symbols.INFO), - fg=Colors.GREEN) + click.secho( + "{} You are connected to the VPN.".format(Symbols.INFO), fg=Colors.GREEN + ) except Exception as e: click.secho(str(e), fg=Colors.RED) raise SystemExit(1) from e @@ -74,38 +85,40 @@ def status(ctx: click.Context, wide: bool = False): def display_vpn_status(wide: bool = False): s = get_tailscale_status() - nodes = s.get('Peer', {}) - nodes.update({"me": s.get('Self')}) + nodes = s.get("Peer", {}) + nodes.update({"me": s.get("Self")}) - headers = ['IP', 'DNS Name', 'OS', 'Online', 'Active'] + headers = ["IP", "DNS Name", "OS", "Online", "Active"] if wide: - headers.extend(['Relay', 'Joined', 'Last Active']) + headers.extend(["Relay", "Joined", "Last Active"]) data = [] for k, v in nodes.items(): row = [ - ",".join(v.get('TailscaleIPs')), + ",".join(v.get("TailscaleIPs")), # removesuffix() is available starting Python 3.9 - v.get('DNSName', '').replace('.' + s.get('MagicDNSSuffix'), ''), - v.get('OS'), - v.get('Online'), - v.get('Active'), + v.get("DNSName", "").replace("." + s.get("MagicDNSSuffix"), ""), + v.get("OS"), + v.get("Online"), + v.get("Active"), ] if wide: - row.extend([ - v.get('Relay'), - v.get('Created'), - v.get('LastSeen'), - ]) - - if k == 'me': + row.extend( + [ + v.get("Relay"), + v.get("Created"), + v.get("LastSeen"), + ] + ) + + if k == "me": row = [click.style(i, fg=Colors.BRIGHT_BLUE) for i in row] data.append(row) tabulate_data(data, headers) click.echo() - click.secho('DNS Suffix: {}'.format(s.get('MagicDNSSuffix'))) + click.secho("DNS Suffix: {}".format(s.get("MagicDNSSuffix"))) click.echo() diff --git a/riocli/vpn/util.py b/riocli/vpn/util.py index f3df0c6c..057288f7 100644 --- a/riocli/vpn/util.py +++ b/riocli/vpn/util.py @@ -33,11 +33,12 @@ from riocli.utils import run_bash, run_bash_with_return_code from riocli.v2client import Client as v2Client -HOSTS_FILE_COMMENT = 'riovpn' +HOSTS_FILE_COMMENT = "riovpn" + def get_host_ip() -> str: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: - s.connect(('8.8.8.8', 80)) + s.connect(("8.8.8.8", 80)) return s.getsockname()[0] @@ -46,37 +47,37 @@ def get_host_name() -> str: def is_linux() -> bool: - return platform.lower() == 'linux' + return platform.lower() == "linux" def is_windows() -> bool: - return platform.lower() == 'win32' + return platform.lower() == "win32" def is_curl_installed() -> bool: - return which('curl') is not None + return which("curl") is not None def is_tailscale_installed() -> bool: - return which('tailscale') is not None + return which("tailscale") is not None def is_tailscale_up() -> bool: - _, code = run_bash_with_return_code('tailscale status') + _, code = run_bash_with_return_code("tailscale status") return code == 0 def get_tailscale_ip() -> str: - return run_bash('tailscale ip') + return run_bash("tailscale ip") def stop_tailscale() -> bool: - _, code = run_bash_with_return_code(priviledged_command('tailscale down')) + _, code = run_bash_with_return_code(priviledged_command("tailscale down")) if code != 0: return False - output, code = run_bash_with_return_code(priviledged_command('tailscale logout')) - if code != 0 and 'no nodekey to log out' not in output: + output, code = run_bash_with_return_code(priviledged_command("tailscale logout")) + if code != 0 and "no nodekey to log out" not in output: return False return True @@ -93,53 +94,59 @@ def install_vpn_tools() -> None: click.confirm( click.style( - '{} VPN tools are not installed. Do you want ' - 'to install them now?'.format(Symbols.INFO), - fg=Colors.YELLOW), - default=True, abort=True) + "{} VPN tools are not installed. Do you want " + "to install them now?".format( + Symbols.INFO + ), + fg=Colors.YELLOW, + ), + default=True, + abort=True, + ) if not is_linux(): - click.secho('Only linux is supported', fg=Colors.YELLOW) + click.secho("Only linux is supported", fg=Colors.YELLOW) raise SystemExit(1) if is_tailscale_installed(): - click.secho('VPN tools already installed', fg=Colors.GREEN) + click.secho("VPN tools already installed", fg=Colors.GREEN) return if not is_curl_installed(): - click.secho('Please install `curl`', fg=Colors.RED) + click.secho("Please install `curl`", fg=Colors.RED) raise SystemExit(1) # download the tailscale install script - run_bash('curl -sLO https://tailscale.com/install.sh') + run_bash("curl -sLO https://tailscale.com/install.sh") with tempfile.TemporaryDirectory() as tmp_dir: - script_path = join(tmp_dir, 'install.sh') + script_path = join(tmp_dir, "install.sh") # move it to the tmp directory - move('install.sh', script_path) + move("install.sh", script_path) if not exists(script_path): raise FileNotFoundError - run_bash('sh {}'.format(script_path)) + run_bash("sh {}".format(script_path)) if not is_tailscale_installed(): - raise Exception('{} Failed to install VPN tools'.format(Symbols.ERROR)) + raise Exception("{} Failed to install VPN tools".format(Symbols.ERROR)) - click.secho('{} VPN tools installed'.format(Symbols.SUCCESS), - fg=Colors.GREEN) + click.secho("{} VPN tools installed".format(Symbols.SUCCESS), fg=Colors.GREEN) def tailscale_ping(tailscale_peer_ip): - cmd = 'tailscale ping --icmp --tsmp --peerapi {}'.format(tailscale_peer_ip) + cmd = "tailscale ping --icmp --tsmp --peerapi {}".format(tailscale_peer_ip) return run_bash_with_return_code(cmd) def is_vpn_enabled_in_project(client: v2Client, project_guid: str) -> bool: project = client.get_project(project_guid) - return (project.status.status.lower() == 'success' and - project.status.vpn.lower() == 'success') + return ( + project.status.status.lower() == "success" + and project.status.vpn.lower() == "success" + ) def priviledged_command(cmd: str) -> str: @@ -147,37 +154,37 @@ def priviledged_command(cmd: str) -> str: if is_windows() or (is_linux() and os.geteuid() == 0): return cmd - return 'sudo {}'.format(cmd) + return "sudo {}".format(cmd) def create_binding( - ctx: click.Context, - name: str = '', - machine: str = '', - labels: dict = {}, - delta: Optional[timedelta] = None, - ephemeral: bool = True, - throwaway: bool = True, + ctx: click.Context, + name: str = "", + machine: str = "", + labels: dict = None, + delta: Optional[timedelta] = None, + ephemeral: bool = True, + throwaway: bool = True, ) -> Munch: - vpn_instance = 'rio-internal-headscale' - if name == '': - name = '{}-{}'.format(ctx.obj.machine_id, int(time.time())) + vpn_instance = "rio-internal-headscale" + if name == "": + name = "{}-{}".format(ctx.obj.machine_id, int(time.time())) body = { - 'metadata': { - 'name': name, - 'labels': labels, + "metadata": { + "name": name, + "labels": labels or {}, + }, + "spec": { + "instance": vpn_instance, + "provider": "headscalevpn", + "throwaway": throwaway, + "config": { + "ephemeral": ephemeral, + "expirationTime": get_key_expiry_time(delta), + "nodeKey": machine, + }, }, - 'spec': { - 'instance': vpn_instance, - 'provider': 'headscalevpn', - 'throwaway': throwaway, - 'config': { - 'ephemeral': ephemeral, - 'expirationTime': get_key_expiry_time(delta), - 'nodeKey': machine, - } - } } client = get_config_from_context(ctx).new_v2_client() @@ -185,7 +192,7 @@ def create_binding( # We may end up creating multiple throwaway tokens in the database. # But that's okay and something that we can live with binding = client.create_instance_binding(vpn_instance, binding=body) - return binding.spec.get('environment', {}) + return binding.spec.get("environment", {}) def get_key_expiry_time(delta: Optional[timedelta]) -> Optional[str]: @@ -193,16 +200,16 @@ def get_key_expiry_time(delta: Optional[timedelta]) -> Optional[str]: return None expiry = datetime.utcnow() + delta - return expiry.isoformat('T') + 'Z' + return expiry.isoformat("T") + "Z" def get_binding_labels() -> dict: return { - 'creator': 'riocli', - 'hostname': get_host_name(), - 'ip_address': str(get_host_ip()), - 'username': getpass.getuser(), - 'rapyuta.io/internal': 'true', + "creator": "riocli", + "hostname": get_host_name(), + "ip_address": str(get_host_ip()), + "username": getpass.getuser(), + "rapyuta.io/internal": "true", } @@ -220,13 +227,13 @@ def update_hosts_file(): device_host_to_name = {} for device in v1_client.get_all_devices(online_device=True): - vpn = device.get('daemons_status').get('vpn') - if vpn and vpn.get('enable') and vpn.get('status') == 'running': + vpn = device.get("daemons_status").get("vpn") + if vpn and vpn.get("enable") and vpn.get("status") == "running": d = v1_client.get_device(device.uuid) - device_host_to_name[d.get('host')] = d.name + device_host_to_name[d.get("host")] = d.name status = get_tailscale_status() - peers = status.get('Peer', {}) + peers = status.get("Peer", {}) hosts = Hosts() @@ -235,16 +242,18 @@ def update_hosts_file(): entries = [] for _, node in peers.items(): - if not node.get('Online'): + if not node.get("Online"): continue - if node.get('HostName') in device_host_to_name: - entries.append(HostsEntry( - entry_type='ipv4', - address=node.get('TailscaleIPs')[0], - names=[device_host_to_name[node.get('HostName')]], - comment=HOSTS_FILE_COMMENT, - )) + if node.get("HostName") in device_host_to_name: + entries.append( + HostsEntry( + entry_type="ipv4", + address=node.get("TailscaleIPs")[0], + names=[device_host_to_name[node.get("HostName")]], + comment=HOSTS_FILE_COMMENT, + ) + ) # Nothing to add if there are no # devices with VPN enabled. @@ -294,8 +303,10 @@ def write_hosts_file(hosts: Hosts) -> None: hosts.write(path=temp.name) temp.close() - run_bash(priviledged_command(f'cp {hosts.path} {hosts.path}.bak')) - _, code = run_bash_with_return_code(priviledged_command(f'mv {temp.name} {hosts.path}')) + run_bash(priviledged_command(f"cp {hosts.path} {hosts.path}.bak")) + _, code = run_bash_with_return_code( + priviledged_command(f"mv {temp.name} {hosts.path}") + ) if code != 0: - raise Exception('failed to write hosts file') + raise Exception("failed to write hosts file")