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..55016311 --- /dev/null +++ b/tests/ui-ext-test.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +VCD="C:\Users\nvladimirovi\AppData\Roaming\Python\Python36\Scripts\vcd.exe" +VCD_UI_EXT_ABS_PATH=D:/test-py-cli/ui_plugin +VCD_HOST=bos1-vcd-sp-static-198-58.eng.vmware.com +VCD_ORG=System +VCD_USER=administrator +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 ui extension by user choice' +$VCD uiext delete diff --git a/utilities/__init__.py b/utilities/__init__.py new file mode 100644 index 00000000..105359db --- /dev/null +++ b/utilities/__init__.py @@ -0,0 +1 @@ +from utilities.colors import Colors \ No newline at end of file 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/ui_ext/cli_spinners/__init__.py b/utilities/ui_ext/cli_spinners/__init__.py new file mode 100644 index 00000000..5c791945 --- /dev/null +++ b/utilities/ui_ext/cli_spinners/__init__.py @@ -0,0 +1 @@ +from utilities.ui_ext.cli_spinners.cli_spinner import CliSpinner \ No newline at end of file diff --git a/utilities/ui_ext/cli_spinners/cli_spinner.py b/utilities/ui_ext/cli_spinners/cli_spinner.py new file mode 100644 index 00000000..d8f6f11f --- /dev/null +++ b/utilities/ui_ext/cli_spinners/cli_spinner.py @@ -0,0 +1,158 @@ +import threading +import time +import sys +from enum import Enum + +Spinners = Enum('Spinners', { + "line": { + "interval": 130, + "frames": [ + "-", + "\\", + "|", + "/" + ] + }, + "dots": { + "interval": 500, + "frames": [ + ".", + "..", + "..." + ] + } +}) + + +def backspace(n): + # print((b'\x08').decode(), end='') # use \x08 char to go back + print('\r', end='') + + +class CliSpinner: + """CliSpinner library. + Attributes + ---------- + CLEAR_LINE : str + Code to clear the line + """ + CLEAR_LINE = '\033[K' + + def __init__(self, text=None, spinner='dots', placement='left', + stream=sys.stdout): + """Constructs the CliSpinner object. + Parameters + ---------- + text : str, optional + Text to display while spinning. + spinner : str|dict, optional + String or dictionary representing spinner. String can be one + of 2 spinners supported. + placement: str, optional + Side of the text to place the spinner on. Can be `left` or `right`. + Defaults to `left`. + stream : io, optional + Output. + """ + self._spinner = Spinners[spinner].value + self.text = text + self._stop_spinner = None + self._interval = self._spinner['interval'] + self._stream = stream + self._frame_index = 0 + self._placement = placement + + def clear(self): + """Clears the line and returns cursor to the start. + of line + Returns + ------- + self + """ + self._stream.write('\r') + self._stream.write(self.CLEAR_LINE) + + return self + + def _frame(self): + """Builds and returns the frame to be rendered + Returns + ------- + frame + """ + frame = self._spinner['frames'][self._frame_index] + self._frame_index += 1 + + if self._frame_index == len(self._spinner['frames']): + self._frame_index = 0 + + return frame + + def _render_frame(self): + """Renders the frame on the line after clearing it. + """ + frame = self._frame() + + if self.text is not None: + output = u'{0} {1}'.format(*[ + (self.text, frame) + if self._placement == 'right' else + (frame, self.text) + ][0]) + else: + output = '{0}'.format(frame) + self._stream.write(output) + backspace(len(output)) + + def _render(self): + """Runs the render until thread flag is set. + Returns + ------- + self + """ + while not self._stop_spinner.is_set(): + self._render_frame() + time.sleep(0.001 * self._interval) + + return self + + def start(self, text=None): + """Starts the spinner on a separate thread. + Parameters + ---------- + text : None, optional + Text to be used alongside spinner + Returns + ------- + self + """ + if text is not None: + self.text = text + + self._stop_spinner = threading.Event() + self._spinner_thread = threading.Thread(target=self._render) + self._spinner_thread.setDaemon(True) + self._render_frame() + self._spinner_id = self._spinner_thread.name + self._spinner_thread.start() + + return self + + def stop(self, message=None): + """Stops the spinner and clears the line. + Returns + ------- + self + """ + if self._spinner_thread: + self._stop_spinner.set() + self._spinner_thread.join() + + self._frame_index = 0 + self._spinner_id = None + self.clear() + + if message is not None: + print(message) + + return self diff --git a/utilities/ui_ext/ext_generator/__init__.py b/utilities/ui_ext/ext_generator/__init__.py new file mode 100644 index 00000000..e318f533 --- /dev/null +++ b/utilities/ui_ext/ext_generator/__init__.py @@ -0,0 +1,2 @@ +from utilities.ui_ext.ext_generator.generator import Generator +from utilities.ui_ext.ext_generator.ext_generator import ExtGenerator \ No newline at end of file diff --git a/utilities/ui_ext/ext_generator/ext_generator.py b/utilities/ui_ext/ext_generator/ext_generator.py new file mode 100644 index 00000000..b1e6dc4f --- /dev/null +++ b/utilities/ui_ext/ext_generator/ext_generator.py @@ -0,0 +1,318 @@ +import os +import queue +import json + +from utilities.ui_ext.prompt_launcher import PromptLauncher, Prompt, ValidatorFactory +from utilities.ui_ext.cli_spinners import CliSpinner +from utilities.ui_ext.ext_generator import Generator +from pathlib import Path +from shutil import copyfile, rmtree + + +class ExtGenerator(Generator): + def __init__(self): + """Constructs the ExtGenerator object. + Returns + ------- + None + """ + + def parse_manifest(self): + """Find manifest.json in the template path, + read the file, parse it to dict and returns it. + If no manifest file is found default dict will be + returned. + Returns + ------- + manifest : dict + """ + + file = None + + try: + file = Path(os.path.join(self.new_project_dir, + 'src/public/manifest.json')) + raise Exception("blabal") + except Exception: + # Assign the file Path obj + file = self.find_file(self.answers["template_path"], + "manifest.json") + + # Check if file is found + if file is not None: + # Read the file + with open(file, "r") as f: + # Parse it to dict and returns it + return json.loads(f.read()) + else: + # Return default manifest values + return { + "urn": "vmware:vcloud:plugin:lifecycle", + "name": "plugin-lifecycle", + "containerVersion": "9.1.0", + "version": "1.0.0", + "scope": ["service-provider"], + "permissions": [], + "description": "", + "vendor": "VMware", + "license": "MIT", + "link": "http://someurl.com", + "module": "SubnavPluginModule", + "route": "plugin-lifecycle" + } + + def populate_manifest(self, basename, original_file_abs_path): + """Open template file, read the file in-memory + and populate the data given from user, creates + the manifest.json with the new data. + Returns + ------- + None + """ + # Read the original file + readFile = open(original_file_abs_path, "r") + + # Create the new file and write the content + with open(basename, "w") as f: + # Parse the json content of the file + files_json = json.loads(readFile.read()) + # Close the readable stream + readFile.close() + + # Loop through answers + for key, value in self.answers.items(): + # If answer key exist in the json + if key in files_json: + # And if it is scope or permissions + if key == 'scope' or key == 'permissions': + # And if there is no content + if len(self.answers[key]) < 1: + # Assign empty array + value = [] + else: + # If it's list already + if isinstance(self.answers[key], list): + value = self.answers[key] + else: + # Create array of strings + value = self.answers[key].split(", ") + + # Assign the value to origin json file + files_json[key] = value + + # Write the new json to the newly created file with sorted keys + # and 4 spaces indentation + f.write(json.dumps(files_json, sort_keys=True, indent=4)) + # Close the writable stream + f.close() + + def generate_files(self, entrie_full_abs_path, new_entrie_path, entrie): + """Generate vCD UI Plugin files, manifest.json + is populated with the data from the questions + with which user has been promped. + Returns + ------- + None + """ + # Base name of the file + basename = os.path.basename(entrie_full_abs_path) + + # If the file is manifest.json + if basename == 'manifest.json': + # Generate new current working directory + newCWD = os.path.join(self.new_project_dir, + new_entrie_path.split(basename)[0][1:]) + oldCWD = os.getcwd() + # Get in new current working directory + os.chdir(newCWD) + # Populate manifest.json file with the data from users answers + self.populate_manifest(basename, + os.path.abspath(entrie.absolute())) + # Reset the current working directory + os.chdir(oldCWD) + else: + # Copy the file from his original directory to his new one + copyfile(os.path.abspath(entrie.absolute()), + os.path.join(self.new_project_dir, new_entrie_path[1:])) + + def copy_files(self, directory=None): + """Generate vCD UI Plugin from template. + Paramenters + ------- + directory | Path + + Returns + ------- + None + """ + + # Create queue for directories + q = queue.Queue() + # Put the start point directory + q.put(directory) + + # Loop while q is not empty + while q.empty() is not True: + # Pop the first element from the queue + node = q.get() + + # Loop through the node entries + for entrie in node.iterdir(): + # Split the absolute path + entrie_full_abs_path = os.path.abspath(entrie.absolute()) + entrie_parts = entrie_full_abs_path.split(self.template_name) + # Get the relative path to the entrie + new_entrie_path = entrie_parts[len(entrie_parts) - 1] + + # If entries is file + if entrie.is_file(): + self.generate_files(entrie_full_abs_path, new_entrie_path, + entrie) + # else if it is directory and it isn't node modules + elif entrie.is_dir() and entrie.name != 'node_modules': + # Create the new directory + os.mkdir(os.path.join(self.new_project_dir, + new_entrie_path[1:])) + + # Add the entrie to the queue to loop through his + # directories if any + q.put(entrie) + + def generate(self): + """Generate vCD UI Plugin from template. + Returns + ------- + None + """ + + # Load inital user prompts + 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" + ) + ]) + + # Assign answers + self.answers = prompts.multi_prompt() + # Assign template name + self.template_name = self.answers["template_path"] + # Construct the path of the new project + self.new_project_dir = os.path.join(os.getcwd(), + self.answers["projectName"]) + + # Indicate that the system search for manifes.json + spinner = CliSpinner(text="Parse manifest.json", spinner='line') + spinner.start() + + # Assign manifest.json + self.manifest = self.parse_manifest() + + # Stop inidaction + spinner.stop() + + # Load prompts + prompts.add([ + Prompt( + "urn", + str, + message="Plugin urn", + default=self.manifest["urn"] + ), + Prompt( + "name", + str, + message="Plugin name", + default=self.manifest["name"] + ), + Prompt( + "containerVersion", + str, + message="Plugin containerVersion", + default=self.manifest["containerVersion"] + ), + Prompt( + "version", + str, + message="Plugin version", + default=self.manifest["version"] + ), + Prompt( + "scope", + str, + message="Plugin scope", + default=self.manifest["scope"] + ), + Prompt( + "permissions", + str, + message="Plugin permissions", + default=self.manifest["permissions"] + ), + Prompt( + "description", + str, + message="Plugin description", + default=self.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=self.manifest["vendor"] + ), + Prompt( + "license", + str, + message="Plugin license", + default=self.manifest["license"] + ), + Prompt( + "link", + str, + message="Plugin link", + default=self.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=self.manifest["route"]) + ]) + # Prompt user + project_answers = prompts.multi_prompt() + + # Collect answers + self.answers = {**self.answers, **project_answers} + + # Indicate generating + spinner = CliSpinner(text="Generate tempalte", spinner='line') + spinner.start() + + # If this path exists remove it + if os.path.exists(self.new_project_dir): + rmtree(self.new_project_dir) + + # Create the directory to given path + os.mkdir(self.new_project_dir) + + # Start copy files + self.copy_files(Path(self.answers["template_path"])) + + # Stop inidaction + spinner.stop("Completed!") diff --git a/utilities/ui_ext/ext_generator/generator.py b/utilities/ui_ext/ext_generator/generator.py new file mode 100644 index 00000000..f474a226 --- /dev/null +++ b/utilities/ui_ext/ext_generator/generator.py @@ -0,0 +1,120 @@ +import os, queue + +from abc import ABC, abstractmethod, abstractstaticmethod +from pathlib import Path + +class Generator(ABC): + @property + def answers(self): + """Getter for answers property. + Returns + ------- + answers : dict + """ + + return self._answers + + @answers.setter + def answers(self, answers): + """Setter for answers property. + Returns + ------- + answers : dict + """ + + self._answers = answers + + @property + def template_name(self): + """Getter for template_name property. + Returns + ------- + template_name : str + """ + + return self._template_name + + @template_name.setter + def template_name(self, template_path): + """Setter for template_name property. + Returns + ------- + template_name : str + """ + + # Split the absolute template path + templates_path_parts = template_path.split("/ui/") + # Assign template name + self._template_name = templates_path_parts[len(templates_path_parts) - 1] + + @property + def new_project_dir(self): + """Getter for new_project_dir property. + Returns + ------- + new_project_dir : str + """ + + return self._new_project_dir + + @new_project_dir.setter + def new_project_dir(self, new_project_dir): + """Setter for new_project_dir property. + Returns + ------- + new_project_dir : str + """ + + self._new_project_dir = new_project_dir + + @abstractmethod + def generate(self): + """Generate vCD UI Plugin from template. + Returns + ------- + None + """ + pass + + def binary_search_file_by_name(self, alist, item): + """Find file with given name in list of files. + Returns + ------- + file : Path + """ + first = 0 + last = len(alist) - 1 + found = False + + while first <= last and not found: + midpoint = int((first + last) / 2) + + if os.path.basename(os.path.abspath(alist[midpoint].absolute())) == item: + found = alist[midpoint] + else: + if item < alist[midpoint].name: + last = midpoint - 1 + else: first = midpoint + 1 + + return found + + def find_file(self, rootPath, fileName): + """Find file with given name in given root directory. + Returns + ------- + file : Path + """ + q = queue.Queue() + q.put(Path(rootPath)) + + while q.empty() is False: + node = q.get() + + found = self.binary_search_file_by_name(list(node.iterdir()), "manifest.json") + + if found is False: + for entrie in node.iterdir(): + if entrie.is_dir(): + q.put(entrie) + else: + return found \ No newline at end of file diff --git a/utilities/ui_ext/prompt_launcher/__init__.py b/utilities/ui_ext/prompt_launcher/__init__.py new file mode 100644 index 00000000..fb9c4130 --- /dev/null +++ b/utilities/ui_ext/prompt_launcher/__init__.py @@ -0,0 +1,3 @@ +from utilities.ui_ext.prompt_launcher.prompt import Prompt +from utilities.ui_ext.prompt_launcher.prompt_launcher import PromptLauncher +from utilities.ui_ext.prompt_launcher.validator_factory import ValidatorFactory \ No newline at end of file diff --git a/utilities/ui_ext/prompt_launcher/prompt.py b/utilities/ui_ext/prompt_launcher/prompt.py new file mode 100644 index 00000000..5c8f3f44 --- /dev/null +++ b/utilities/ui_ext/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/ui_ext/prompt_launcher/prompt_launcher.py b/utilities/ui_ext/prompt_launcher/prompt_launcher.py new file mode 100644 index 00000000..84a9a58e --- /dev/null +++ b/utilities/ui_ext/prompt_launcher/prompt_launcher.py @@ -0,0 +1,105 @@ +import click + +from utilities 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/utilities/ui_ext/prompt_launcher/validator_factory.py b/utilities/ui_ext/prompt_launcher/validator_factory.py new file mode 100644 index 00000000..5182aa25 --- /dev/null +++ b/utilities/ui_ext/prompt_launcher/validator_factory.py @@ -0,0 +1,16 @@ +from abc import ABC, abstractstaticmethod +from utilities.ui_ext.prompt_launcher.validators import ValidateFolderExistence, LengthValidator, PatternValidator + + +class ValidatorFactory(ABC): + @abstractstaticmethod + def checkForFolderExistence(): + return ValidateFolderExistence() + + @abstractstaticmethod + def pattern(pattern): + return PatternValidator(pattern) + + @abstractstaticmethod + def length(minL, maxL): + return LengthValidator(minL=minL, maxL=maxL) \ No newline at end of file diff --git a/utilities/ui_ext/prompt_launcher/validators/__init__.py b/utilities/ui_ext/prompt_launcher/validators/__init__.py new file mode 100644 index 00000000..019cc7b1 --- /dev/null +++ b/utilities/ui_ext/prompt_launcher/validators/__init__.py @@ -0,0 +1,4 @@ +from utilities.ui_ext.prompt_launcher.validators.folder_validator import ValidateFolderExistence +from utilities.ui_ext.prompt_launcher.validators.length_validator import LengthValidator +from utilities.ui_ext.prompt_launcher.validators.pattern_validator import PatternValidator +from utilities.ui_ext.prompt_launcher.validators.validator import Validator \ No newline at end of file diff --git a/utilities/ui_ext/prompt_launcher/validators/folder_validator.py b/utilities/ui_ext/prompt_launcher/validators/folder_validator.py new file mode 100644 index 00000000..fb1c9423 --- /dev/null +++ b/utilities/ui_ext/prompt_launcher/validators/folder_validator.py @@ -0,0 +1,16 @@ +import os + +from utilities.ui_ext.prompt_launcher.validators.validator import Validator + + +class ValidateFolderExistence(Validator): + def validate(self, input=None): + """Check given directory for existence + Returns + ------- + input : any + """ + if os.path.exists(input): + return input + + return None diff --git a/utilities/ui_ext/prompt_launcher/validators/length_validator.py b/utilities/ui_ext/prompt_launcher/validators/length_validator.py new file mode 100644 index 00000000..7b4b26c5 --- /dev/null +++ b/utilities/ui_ext/prompt_launcher/validators/length_validator.py @@ -0,0 +1,22 @@ +from utilities.ui_ext.prompt_launcher.validators.validator import Validator + + +class LengthValidator(Validator): + def __init__(self, minL=0, maxL=0): + self.minL = minL + self.maxL = maxL + + def validate(self, input=None): + """Check input length. + Parameters + ------- + minL | integer + maxL | integer + Returns + ------- + input : any + """ + if len(input) >= self.minL and len(input) <= self.maxL: + return input + + return None diff --git a/utilities/ui_ext/prompt_launcher/validators/pattern_validator.py b/utilities/ui_ext/prompt_launcher/validators/pattern_validator.py new file mode 100644 index 00000000..e412d50d --- /dev/null +++ b/utilities/ui_ext/prompt_launcher/validators/pattern_validator.py @@ -0,0 +1,19 @@ +import re + +from utilities.ui_ext.prompt_launcher.validators.validator import Validator + + +class PatternValidator(Validator): + def __init__(self, pattern): + self.pattern = pattern + + def validate(self, input=None): + """Check input structure agains given pattern. + Returns + ------- + input : any + """ + if re.match(self.pattern, input): + return input + + return None diff --git a/utilities/ui_ext/prompt_launcher/validators/validator.py b/utilities/ui_ext/prompt_launcher/validators/validator.py new file mode 100644 index 00000000..702054dc --- /dev/null +++ b/utilities/ui_ext/prompt_launcher/validators/validator.py @@ -0,0 +1,6 @@ +from abc import ABC + + +class Validator(ABC): + def validate(self, input=None): + pass diff --git a/vcd_cli/ui_extensions.py b/vcd_cli/ui_extensions.py new file mode 100644 index 00000000..cc91f2fb --- /dev/null +++ b/vcd_cli/ui_extensions.py @@ -0,0 +1,139 @@ +# 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.ui_ext.cli_spinners import CliSpinner +from utilities.ui_ext.ext_generator import ExtGenerator +from pyvcloud.vcd.ui_plugin import UiPlugin +from vcd_cli.utils import restore_session +from vcd_cli.utils import stderr +from vcd_cli.vcd import vcd + + +def get_ext_id_dynamically(): + return click.prompt( + "Enter the id of the UI extension which you want to delete", + type=str + ) + + +@vcd.group(short_help='Manage UI Extensions') +@click.pass_context +def uiext(ctx): + """Manage UI Extensions in vCloud Director. + +\b + Examples + vcd ext list + Get list of Extensions for current tenant. +\b + vcd ext 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 ext 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() + except Exception as e: + stderr(e, ctx) + + +@uiext.command('list', short_help='list UI Extensions') +@click.pass_context +def list_extensions(ctx): + try: + spinner = CliSpinner(text="Listing", spinner="dots", placement="right") + spinner.start() + + restore_session(ctx, vdc_required=False) + client = ctx.obj['client'] + token = client._session.headers['x-vcloud-authorization'] + base_uri = client._uri.split("/api")[0] + ui = UiPlugin(base_uri, token) + + spinner.stop() + + 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: + spinner = CliSpinner(text="Deploy", spinner="line") + spinner.start() + + restore_session(ctx, vdc_required=False) + client = ctx.obj['client'] + token = client._session.headers['x-vcloud-authorization'] + base_uri = client._uri.split("/api")[0] + ui = UiPlugin(base_uri, token) + + if path: + if os.path.exists(path) is False: + spinner.stop() + raise FileNotFoundError() + + ui.deploy(path, publish, preview) + spinner.stop("Completed!") + else: + ui.deploy(os.getcwd(), publish, preview) + spinner.stop("Completed!") + 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: + spinner = CliSpinner(text="Delete", spinner="line") + spinner.start() + + restore_session(ctx, vdc_required=False) + client = ctx.obj['client'] + token = client._session.headers['x-vcloud-authorization'] + base_uri = client._uri.split("/api")[0] + ui = UiPlugin(base_uri, token) + ui.delete(specific=extension, deleteAll=all) + spinner.stop() + 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)