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..476db662 --- /dev/null +++ b/vcd_cli/ui_extensions.py @@ -0,0 +1,214 @@ +# 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'] + ui = UiPlugin(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'] + ui = UiPlugin(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'] + ui = UiPlugin(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)