From 9269fa40214d577615c6cad1ff8bf28c36d685e0 Mon Sep 17 00:00:00 2001 From: Nikola Vladimirov Iliev Date: Thu, 20 Sep 2018 14:17:02 +0300 Subject: [PATCH] Add ui extensions commands Following commads are added: generate, list, deploy, delete. They will help the user to manage his ui extensions. Prompt and PromptLauncher classes are build on top of python click lib to give the ability to validate and collect the data from the user. Signed-off-by: Nikola Vladimirov Iliev --- .gitignore | 1 + .travis.yml | 1 + tests/run-tests.sh | 1 + tests/ui-ext-test.sh | 21 ++ utilities/colors.py | 12 ++ utilities/prompt_launcher/__init__.py | 2 + utilities/prompt_launcher/prompt.py | 149 +++++++++++++ utilities/prompt_launcher/prompt_launcher.py | 105 +++++++++ vcd_cli/ui_extensions.py | 216 +++++++++++++++++++ vcd_cli/vcd.py | 1 + 10 files changed, 509 insertions(+) create mode 100644 tests/ui-ext-test.sh create mode 100644 utilities/colors.py create mode 100644 utilities/prompt_launcher/__init__.py create mode 100644 utilities/prompt_launcher/prompt.py create mode 100644 utilities/prompt_launcher/prompt_launcher.py create mode 100644 vcd_cli/ui_extensions.py diff --git a/.gitignore b/.gitignore index cc1a13b5..dcc56e4b 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ docs/_build/ target/ .idea/ +.vscode/ docs/.bundle docs/_site diff --git a/.travis.yml b/.travis.yml index 263846af..ea486cb6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: install: - pip install git+https://github.com/vmware/pyvcloud.git - pip install -r requirements.txt + - pip install -e . - python setup.py install - pip install tox diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 1c6ec097..a846e5e2 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -6,3 +6,4 @@ D=`dirname $0` $D/tenant-onboard.sh $D/tenant-operations.sh $D/cleanup.sh +$D/ui-ext-test.sh diff --git a/tests/ui-ext-test.sh b/tests/ui-ext-test.sh new file mode 100644 index 00000000..e2e77e4b --- /dev/null +++ b/tests/ui-ext-test.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +VCD="path/to/vcd/exe" +VCD_UI_EXT_ABS_PATH=path/to/existing/ui/plugin +VCD_HOST=host.vmware.com +VCD_ORG=Org +VCD_USER=user +VCD_PASSWORD='********' + +# $VCD login $VCD_HOST $VCD_ORG $VCD_USER --password $VCD_PASSWORD + +$VCD version + +echo 'This should deploy ui extension' +$VCD uiext deploy --path $VCD_UI_EXT_ABS_PATH -p -pr +echo 'This should list all ui extensions' +$VCD uiext list +echo 'This should delete all ui extension' +$VCD uiext delete --all diff --git a/utilities/colors.py b/utilities/colors.py new file mode 100644 index 00000000..cbd71bd4 --- /dev/null +++ b/utilities/colors.py @@ -0,0 +1,12 @@ +from enum import Enum + +Colors = Enum('Colors', { + 'HEADER': '\033[95m', + 'OKBLUE': '\033[94m', + 'OKGREEN': '\033[92m', + 'WARNING': '\033[93m', + 'FAIL': '\033[91m', + 'ENDC': '\033[0m', + 'BOLD': '\033[1m', + 'UNDERLINE': '\033[4m' +}) diff --git a/utilities/prompt_launcher/__init__.py b/utilities/prompt_launcher/__init__.py new file mode 100644 index 00000000..b549dcf6 --- /dev/null +++ b/utilities/prompt_launcher/__init__.py @@ -0,0 +1,2 @@ +from utilities.prompt_launcher.prompt_launcher import PromptLauncher +from utilities.prompt_launcher.prompt import Prompt diff --git a/utilities/prompt_launcher/prompt.py b/utilities/prompt_launcher/prompt.py new file mode 100644 index 00000000..5c8f3f44 --- /dev/null +++ b/utilities/prompt_launcher/prompt.py @@ -0,0 +1,149 @@ +class Prompt: + """Represent abstraction of python click + propmt data. + """ + def __init__(self, name, type, message=None, default=None, validator=None, + err_message=None): + self.name = name + self.type = type + self.message = message + self.default = default + self.validator = validator + self.error_message = err_message + + @property + def name(self): + """Getter for name property. + Returns + ------- + name : str + name value + """ + return self._name + + @name.setter + def name(self, name): + """Setter for name property. + Parameters + ---------- + name : str + Defines the name value + """ + + if len(name) < 1: + raise Exception("""The name property can not be + with lenght less ten 1.""") + + self._name = name + + @property + def type(self): + """Getter for type property. + Returns + ------- + type | str + type value + """ + return self._type + + @type.setter + def type(self, type): + """Setter for type property. + Parameters + ---------- + type : str + Defines the name value + """ + + self._type = type + + @property + def message(self): + """Getter for message property. + Returns + ------- + message | str + message value + """ + + return self._message + + @message.setter + def message(self, message=None): + """Setter for message property. + Parameters + ---------- + message : str + Defines the message value + """ + + if type(message) is not str: + raise Exception("The message property has to be string") + + self._message = message + + @property + def default(self): + """Getter for default property. + Returns + ------- + default | any + default value + """ + + return self._default + + @default.setter + def default(self, default): + """Setter for default property. + Parameters + ---------- + default : any + Defines the message value + """ + + self._default = default + + @property + def validator(self): + """Getter for validator property. + Returns + ------- + validator | Validator / Validator[] + validator value + """ + + return self._validator + + @validator.setter + def validator(self, validator): + """Setter for validator property. + Parameters + ---------- + validator : Validator | Validator[] + Defines the validator value + """ + + self._validator = validator + + @property + def error_message(self): + """Getter for error_message property. + Returns + ------- + error_message | boolean + error_message value + """ + return self._error_message + + @error_message.setter + def error_message(self, err_message): + """Setter for error_message property. + Parameters + ---------- + error_message : boolean + Defines the error_message value + """ + + if err_message is not None: + self._error_message = err_message diff --git a/utilities/prompt_launcher/prompt_launcher.py b/utilities/prompt_launcher/prompt_launcher.py new file mode 100644 index 00000000..3c027f2c --- /dev/null +++ b/utilities/prompt_launcher/prompt_launcher.py @@ -0,0 +1,105 @@ +import click + +from utilities.colors import Colors + + +class PromptLauncher: + """Collect and execute prompt objects. + """ + def __init__(self, prompts=[]): + """Constructs the PromptLauncher object. + Paramenters + ------- + prompts | Prompt + + click | Click ( lib for python CLI ) + + Returns + ------- + None + """ + self._prompts = prompts + + def add(self, propmts): + """Add new items to dict with concat + Returns + ------- + None + """ + + self._prompts = self._prompts + propmts + + def pop_prompt(self): + """Pop the first element from the prompts list. + Returns + ------- + prompt | Prompt + prompt value + """ + return self._prompts.pop(0) + + def recuresive_prompt(self, prompt, valuedict): + return self.prompt(prompt, valuedict) + + def prompt(self, prompt, valuedict={}): + """Execute given prompt object. + Returns + ------- + valuedict | Dictionary + valuedict value + """ + + if prompt.validator: + value = None + + err_msg = prompt.error_message + fail_clr = Colors['FAIL'].value + reset_clr = Colors['ENDC'].value + + if type(prompt.validator) is list: + userInput = click.prompt( + prompt._message, + type=prompt.type, + default=prompt.default + ) + + for validator in prompt.validator: + validator_return = validator.validate(userInput) + + if validator_return is None: + print(Colors['FAIL'].value + err_msg + reset_clr) + return self.recuresive_prompt(prompt, valuedict) + + value = validator_return + else: + value = prompt.validator.validate(input=click.prompt( + prompt._message, + type=prompt.type, + default=prompt.default)) + if value is not None: + valuedict[prompt.name] = value + else: + print(fail_clr + err_msg + reset_clr) + return self.recuresive_prompt(prompt, valuedict) + else: + valuedict[prompt.name] = click.prompt( + prompt._message, + type=prompt.type, + default=prompt.default, + ) + + return valuedict + + def multi_prompt(self): + """Prompt the user with questions. + Returns + ------- + thisdict | Dictionary + thisdict value + """ + thisdict = {} + + while len(self._prompts) > 0: + prompt = self._prompts.pop(0) + thisdict = self.prompt(prompt, thisdict) + return thisdict diff --git a/vcd_cli/ui_extensions.py b/vcd_cli/ui_extensions.py new file mode 100644 index 00000000..65c13e45 --- /dev/null +++ b/vcd_cli/ui_extensions.py @@ -0,0 +1,216 @@ +# vCloud CLI 0.1 +# +# Copyright (c) 2017-2018 VMware, Inc. All Rights Reserved. +# +# This product is licensed to you under the +# Apache License, Version 2.0 (the "License"). +# You may not use this product except in compliance with the License. +# +# This product may include a number of subcomponents with +# separate copyright notices and license terms. Your use of the source +# code for the these subcomponents is subject to the terms and +# conditions of the subcomponent's license, as noted in the LICENSE file. +# + +import click +import pprint +import os + +from utilities.prompt_launcher import PromptLauncher, Prompt +from pyvcloud.vcd.ui_plugin import UiPlugin +from pyvcloud.vcd.ext_generator import ExtGenerator +from pyvcloud.vcd.validator_factory import ValidatorFactory +from vcd_cli.utils import restore_session +from vcd_cli.utils import stderr +from vcd_cli.vcd import vcd + + +def projectPromptsFn(): + prompts = PromptLauncher([ + Prompt( + "template_path", + str, + message="Please enter a valid absolute template path", + validator=ValidatorFactory.checkForFolderExistence(), + err_message="The path has to be valid absolute path" + ), + Prompt( + "projectName", + str, + message="Project name", + default="ui_plugin" + ) + ]) + + return prompts.multi_prompt() + + +def pluginPromptsFn(manifest): + prompts = PromptLauncher([ + Prompt( + "urn", + str, + message="Plugin urn", + default=manifest["urn"] + ), + Prompt( + "name", + str, + message="Plugin name", + default=manifest["name"] + ), + Prompt( + "containerVersion", + str, + message="Plugin containerVersion", + default=manifest["containerVersion"] + ), + Prompt( + "version", + str, + message="Plugin version", + default=manifest["version"] + ), + Prompt( + "scope", + str, + message="Plugin scope", + default=manifest["scope"] + ), + Prompt( + "permissions", + str, + message="Plugin permissions", + default=manifest["permissions"] + ), + Prompt( + "description", + str, + message="Plugin description", + default=manifest["description"], + err_message="""Plugin description has to be greather then 3 + and less then 255 characters""", + validator=ValidatorFactory.length(0, 255), + ), + Prompt( + "vendor", + str, + message="Plugin vendor", + default=manifest["vendor"] + ), + Prompt( + "license", + str, + message="Plugin license", + default=manifest["license"] + ), + Prompt( + "link", + str, + message="Plugin link", + default=manifest["link"], + err_message="""The link url is not valid, please enter valid url + address and with length between 8 - 100 characters.""", + validator=[ + ValidatorFactory.length(8, 100), + ValidatorFactory.pattern(r'^((http|https)://)') + ] + ), + Prompt("route", str, message="Plugin route", + default=manifest["route"]) + ]) + + return prompts.multi_prompt() + + +@vcd.group(short_help='Manage UI Extensions') +@click.pass_context +def uiext(ctx): + """Manage UI Extensions in vCloud Director. + +\b + Examples + vcd uiext list + Get list of Extensions for current tenant. +\b + vcd uiext deploy --publish ( -p ) --preview ( -pr ) --path + Deploy extension to vCD and if this extension already exists it + will be replaced and/or be published for all tenants. + If --preview ( -pr ) flag is provided user will see in his command + line his UI extension configuration. + You can specify absolute path to your UI extension root directory + if not path given current working directory will + be considered as UI extension root directory. +\b + vcd uiext delete --all ( -a ) --extension + Delete specific or all plugins, if no flags or options are + provided the user will be promped for UI extension id. + """ + pass + + +@uiext.command('generate', short_help='generate UI Extension') +@click.pass_context +def generate_extension(ctx): + try: + gen = ExtGenerator() + gen.generate(projectPromptsFn, pluginPromptsFn) + except Exception as e: + stderr(e, ctx) + + +@uiext.command('list', short_help='list UI Extensions') +@click.pass_context +def list_extensions(ctx): + try: + restore_session(ctx, vdc_required=False) + client = ctx.obj['client'] + base_uri = client._uri.split("/api")[0] + ui = UiPlugin(base_uri, client) + + pprint.pprint((ui.getUiExtensions().json())) + except Exception as e: + stderr(e, ctx) + + +@uiext.command('deploy', short_help="""Deploy extension to vCD and if this +extension already exists it +will be replaced and/or be published for all tenants""") +@click.pass_context +@click.option('--path', default=None, type=str) +@click.option('--publish', '-p', default=False, is_flag=True) +@click.option('--preview', '-pr', default=False, is_flag=True) +def deploy(ctx, path, publish, preview): + try: + restore_session(ctx, vdc_required=False) + client = ctx.obj['client'] + base_uri = client._uri.split("/api")[0] + ui = UiPlugin(base_uri, client) + result = None + + if path: + if os.path.exists(path) is False: + raise FileNotFoundError() + result = ui.deploy(path, publish, preview) + else: + result = ui.deploy(os.getcwd(), publish, preview) + + if result is not None: + print(result) + except Exception as e: + stderr(e, ctx) + + +@uiext.command('delete', short_help='Delete one or all extensions from vCD') +@click.pass_context +@click.option('--extension', default=None, help='Enter UI extension id.') +@click.option('--all', '-a', default=False, is_flag=True) +def delete(ctx, extension, all): + try: + restore_session(ctx, vdc_required=False) + client = ctx.obj['client'] + base_uri = client._uri.split("/api")[0] + ui = UiPlugin(base_uri, client) + ui.delete(specific=extension, deleteAll=all) + except Exception as e: + stderr(e, ctx) diff --git a/vcd_cli/vcd.py b/vcd_cli/vcd.py index a5e1d30e..486910a8 100644 --- a/vcd_cli/vcd.py +++ b/vcd_cli/vcd.py @@ -129,4 +129,5 @@ def help(ctx, tree): from vcd_cli import vc # NOQA from vcd_cli import vdc # NOQA from vcd_cli import vm # NOQA + from vcd_cli import ui_extensions # NOQA init(autoreset=True)