def get_fqn_for_name(namespaces, name): """Analyze name for namespace reference. If namespaces are used - return a full name :param namespaces: content of 'Namespaces' section of muranoPL class :param name: name that should be checked :returns: generated name according to namespaces """ values = name.split(':') if len(values) == 1: if '=' in namespaces: return namespaces['='] + '.' + values[0] return values[0] if len(values) > 2: raise exceptions.CommandError( "Error in class definition: Wrong usage of ':' is " "reserved for namespace referencing and could " "be used only once " "for each name") if not namespaces: raise exceptions.CommandError("Error in {0} class definition: " "'Namespaces' section is missed") result = namespaces.get(values[0]) if not result: raise exceptions.CommandError( "Error in class definition: namespaces " "reference is not correct at the 'Extends'" " section") return result + '.' + values[1]
def do_package_create(mc, args): """Create an application package.""" if args.template and args.classes_dir: raise exceptions.CommandError( "Provide --template for a HOT-based package, OR" " --classes-dir for a MuranoPL-based package") if not args.template and not args.classes_dir: raise exceptions.CommandError( "Provide --template for a HOT-based package, OR at least" " --classes-dir for a MuranoPL-based package") directory_path = None try: archive_name = args.output if args.output else None if args.template: directory_path = hot_package.prepare_package(args) if not archive_name: archive_name = os.path.basename(args.template) archive_name = os.path.splitext(archive_name)[0] + ".zip" else: directory_path = mpl_package.prepare_package(args) if not archive_name: archive_name = tempfile.mkstemp(prefix="murano_", dir=os.getcwd())[1] + ".zip" _make_archive(archive_name, directory_path) print("Application package is available at " + os.path.abspath(archive_name)) finally: if directory_path: shutil.rmtree(directory_path)
def update_args(args): """Add and update arguments if possible. Some parameters are not required and would be guessed from muranoPL classes: thus, if class extends system application class fully qualified and require names could be calculated. Also, in that case type of a package could be set to 'Application'. """ classes = {} extends_from_application = False for root, dirs, files in os.walk(args.classes_dir): for class_file in files: class_file_path = os.path.join(root, class_file) try: with open(class_file_path) as f: content = yaml.load(f, utils.YaqlYamlLoader) if not content.get('Name'): raise exceptions.CommandError( "Error in class definition: 'Name' " "section is required") class_name = get_fqn_for_name(content.get('Namespaces'), content['Name']) if root == args.classes_dir: relative_path = class_file else: relative_path = os.path.join( root.replace(args.classes_dir, "")[1:], class_file) classes[class_name] = relative_path extends_from_application = check_derived_from_application( content, extends_from_application) if extends_from_application: if not args.type: args.type = 'Application' if not args.name: args.name = class_name.split('.')[-1] if not args.full_name: args.full_name = class_name except yaml.YAMLError: raise exceptions.CommandError( "MuranoPL class {0} should be" " a valid yaml file".format(class_file_path)) except IOError: raise exceptions.CommandError( "Could not open file {0}".format(class_file_path)) if not classes: raise exceptions.CommandError("Application should have " "at least one class") args.classes = classes return args
def do_app_show(mc, args): """List applications, added to specified environment. """ if args.path == '/': apps = mc.services.list(args.id) formatters = { 'id': lambda x: getattr(x, '?')['id'], 'type': lambda x: getattr(x, '?')['type'] } field_labels = ['Id', 'Name', 'Type'] fields = ['id', 'name', 'type'] utils.print_list(apps, fields, field_labels, formatters=formatters) else: if not args.path.startswith('/'): args.path = '/' + args.path app = mc.services.get(args.id, args.path) # If app with specified path is not found, it is empty. if hasattr(app, '?'): formatters = {} for key in app.to_dict().keys(): formatters[key] = utils.json_formatter utils.print_dict(app.to_dict(), formatters) else: raise exceptions.CommandError("Could not find application at path" " %s" % args.path)
def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog abandon = getattr(parsed_args, 'abandon', False) failure_count = 0 for environment_id in parsed_args.id: try: environment = murano_utils.find_resource( client.environments, environment_id) client.environments.delete(environment.id, abandon) except exceptions.NotFound: failure_count += 1 print("Failed to delete '{0}'; environment not found".format( environment_id)) if failure_count == len(parsed_args.id): raise exceptions.CommandError("Unable to find and delete any of " "the specified environments.") data = client.environments.list() columns = ('id', 'name', 'status', 'created', 'updated') column_headers = [c.capitalize() for c in columns] return (column_headers, list(utils.get_item_properties( s, columns, ) for s in data))
def do_package_show(mc, args): """Display details for a package.""" try: package = mc.packages.get(args.id) except common_exceptions.HTTPNotFound: raise exceptions.CommandError("Package %s not found" % args.id) else: to_display = dict(id=package.id, type=package.type, owner_id=package.owner_id, name=package.name, fully_qualified_name=package.fully_qualified_name, is_public=package.is_public, enabled=package.enabled, class_definitions=", ".join( package.class_definitions), categories=", ".join(package.categories), tags=", ".join(package.tags), description=package.description) formatters = { 'class_definitions': utils.text_wrap_formatter, 'categories': utils.text_wrap_formatter, 'tags': utils.text_wrap_formatter, 'description': utils.text_wrap_formatter, } utils.print_dict(to_display, formatters)
def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog arguments = {} for argument in parsed_args.arguments or []: if '=' not in argument: raise exceptions.CommandError( "Argument should be in form of KEY=VALUE. Found: " "{0}".format(argument)) key, value = argument.split('=', 1) try: value = json.loads(value) except ValueError: # treat value as a string if it doesn't load as json pass arguments[key] = value request_body = { "className": parsed_args.class_name, "methodName": parsed_args.method_name, "packageName": parsed_args.package_name or None, "classVersion": parsed_args.class_version or '=0', "parameters": arguments } print("Waiting for result...") result = client.static_actions.call(request_body).get_result() return ["Static action result"], [result]
def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog failure_count = 0 for category_id in parsed_args.id: try: client.categories.delete(category_id) except Exception: failure_count += 1 print("Failed to delete '{0}'; category not found".format( category_id)) if failure_count == len(parsed_args.id): raise exceptions.CommandError("Unable to find and delete any of " "the specified categories.") data = client.categories.list() fields = ["id", "name"] field_labels = ["ID", "Name"] return (field_labels, list(utils.get_item_properties( s, fields, ) for s in data))
def _discover_auth_versions(self, session, auth_url): # discover the API versions the server is supporting base on the # given URL v2_auth_url = None v3_auth_url = None try: ks_discover = discover.Discover(session=session, auth_url=auth_url) v2_auth_url = ks_discover.url_for('2.0') v3_auth_url = ks_discover.url_for('3.0') except ks_exc.ClientException as e: # Identity service may not support discover API version. # Lets trying to figure out the API version from the original URL. url_parts = urlparse.urlparse(auth_url) (scheme, netloc, path, params, query, fragment) = url_parts path = path.lower() if path.startswith('/v3'): v3_auth_url = auth_url elif path.startswith('/v2'): v2_auth_url = auth_url else: # not enough information to determine the auth version msg = ('Unable to determine the Keystone version ' 'to authenticate with using the given ' 'auth_url. Identity service may not support API ' 'version discovery. Please provide a versioned ' 'auth_url instead. error=%s') % (e) raise exc.CommandError(msg) return (v2_auth_url, v3_auth_url)
def prepare_package(args): """Compose required files for murano application package. :param args: list of command line arguments :returns: absolute path to directory with prepared files """ manifest = generate_manifest(args) temp_dir = tempfile.mkdtemp() manifest_file = os.path.join(temp_dir, 'manifest.yaml') template_file = os.path.join(temp_dir, 'template.yaml') if args.resources_dir: if not os.path.isdir(args.resources_dir): raise exceptions.CommandError( "'--resources-dir' parameter should be a directory") resource_directory = os.path.join(temp_dir, 'Resources') shutil.copytree(args.resources_dir, resource_directory) logo_file = os.path.join(temp_dir, 'logo.png') if not args.logo: shutil.copyfile(muranoclient.get_resource('heat_logo.png'), logo_file) else: if os.path.isfile(args.logo): shutil.copyfile(args.logo, logo_file) with open(manifest_file, 'w') as f: f.write(yaml.dump(manifest, default_flow_style=False)) shutil.copyfile(args.template, template_file) return temp_dir
def generate_manifest(args): """Generates application manifest file. If some parameters are missed - they we be generated automatically. :param args: :returns: dictionary, contains manifest file data """ if not os.path.isfile(args.template): raise exceptions.CommandError("Template '{0}' doesn`t exist".format( args.template)) filename = os.path.basename(args.template) if not args.name: args.name = os.path.splitext(filename)[0] if not args.full_name: prefix = 'io.murano.apps.generated' normalized_name = args.name.replace('_', ' ').replace('-', ' ') normalized_name = normalized_name.title().replace(' ', '') args.full_name = '{0}.{1}'.format(prefix, normalized_name) try: with open(args.template, 'rb') as heat_file: yaml_content = yaml.load(heat_file) if not args.description: args.description = yaml_content.get( 'description', 'Heat-defined application for a template "{0}"'.format( filename)) except yaml.YAMLError: raise exceptions.CommandError( "Heat template, represented by --'template' parameter" " should be a valid yaml file") if not args.author: args.author = args.os_username if not args.tags: args.tags = ['Heat-generated'] manifest = { 'Format': 'Heat.HOT/1.0', 'Type': 'Application', 'FullName': args.full_name, 'Name': args.name, 'Description': args.description, 'Author': args.author, 'Tags': args.tags } return manifest
def prepare_package(args): """Prepare for application package Prepare all files and directories for that application package. Generates manifest file and all required parameters for that. :param args: list of command line arguments :returns: absolute path to directory with prepared files """ if args.type and args.type not in ['Application', 'Library']: raise exceptions.CommandError( "--type should be set to 'Application' or 'Library'") manifest = generate_manifest(args) if args.type == 'Application': if not args.ui: raise exceptions.CommandError("'--ui' is required parameter") if not os.path.exists(args.ui) or not os.path.isfile(args.ui): raise exceptions.CommandError( "{0} is not a file or doesn`t exist".format(args.ui)) temp_dir = tempfile.mkdtemp() manifest_file = os.path.join(temp_dir, 'manifest.yaml') classes_directory = os.path.join(temp_dir, 'Classes') resource_directory = os.path.join(temp_dir, 'Resources') with open(manifest_file, 'w') as f: f.write(yaml.dump(manifest, default_flow_style=False)) logo_file = os.path.join(temp_dir, 'logo.png') if not args.logo or (args.logo and not os.path.isfile(args.logo)): shutil.copyfile(muranoclient.get_resource('mpl_logo.png'), logo_file) else: shutil.copyfile(args.logo, logo_file) shutil.copytree(args.classes_dir, classes_directory) if args.resources_dir: if not os.path.isdir(args.resources_dir): raise exceptions.CommandError( "'--resources-dir' parameter should be a directory") shutil.copytree(args.resources_dir, resource_directory) if args.ui: ui_directory = os.path.join(temp_dir, 'UI') os.mkdir(ui_directory) shutil.copyfile(args.ui, os.path.join(ui_directory, 'ui.yaml')) return temp_dir
def do_environment_rename(mc, args): """Rename an environment.""" try: environment = utils.find_resource(mc.environments, args.id) environment = mc.environments.update(environment.id, args.name) except exceptions.NotFound: raise exceptions.CommandError("Environment %s not found" % args.id) else: _print_environment_list([environment])
def do_package_delete(mc, args): """Delete a package.""" failure_count = 0 for package_id in args.id: try: mc.packages.delete(package_id) print("Deleted package '{0}'".format(package_id)) except exceptions.NotFound: raise exceptions.CommandError("Package %s not found" % package_id) failure_count += 1 print( "Failed to delete '{0}'; package not found".format(package_id)) if failure_count == len(args.id): raise exceptions.CommandError("Unable to find and delete any of the " "specified packages.") else: do_package_list(mc)
def _handle_package_exists(mc, data, package, exists_action): name = package.manifest['FullName'] version = package.manifest.get('Version', '0') while True: print("Importing package {0}".format(name)) try: return mc.packages.create(data, {name: package.file()}) except common_exceptions.HTTPConflict: print( "Importing package {0} failed. Package with the same" " name/classes is already registered.".format(name)) allowed_results = ['s', 'u', 'a'] res = exists_action if not res: while True: print("What do you want to do? (s)kip, (u)pdate, (a)bort") res = six.moves.input() if res in allowed_results: break if res == 's': print("Skipping.") return None elif res == 'a': print("Exiting.") sys.exit() elif res == 'u': pkgs = list( mc.packages.filter(fqn=name, version=version, owned=True)) if not pkgs: msg = ( "Got a conflict response, but could not find the " "package '{0}' in the current tenant.\nThis probably " "means the conflicting package is in another tenant.\n" "Please delete it manually.").format(name) raise exceptions.CommandError(msg) elif len(pkgs) > 1: msg = ( "Got {0} packages with name '{1}'.\nI do not trust " "myself, please delete the package manually.").format( len(pkgs), name) raise exceptions.CommandError(msg) print("Deleting package {0}({1})".format(name, pkgs[0].id)) mc.packages.delete(pkgs[0].id) continue
def do_help(self, args): """Display help about this program or one of its subcommands.""" if getattr(args, 'command', None): if args.command in self.subcommands: self.subcommands[args.command].print_help() else: msg = "'%s' is not a valid subcommand" raise exc.CommandError(msg % args.command) else: self.parser.print_help()
def do_package_download(mc, args): """Download a package to a filename or stdout.""" def download_to_fh(package_id, fh): fh.write(mc.packages.download(package_id)) try: if args.filename: with open(args.filename, 'wb') as fh: download_to_fh(args.id, fh) print("Package downloaded to %s" % args.filename) elif not sys.stdout.isatty(): download_to_fh(args.id, sys.stdout) else: msg = ('No stdout redirection or local file specified for ' 'downloaded package. Please specify a local file to save ' 'downloaded package or redirect output to another source.') raise exceptions.CommandError(msg) except common_exceptions.HTTPNotFound: raise exceptions.CommandError("Package %s not found" % args.id)
def do_deployment_list(mc, args): """List deployments for an environment.""" try: environment = utils.find_resource(mc.environments, args.id) deployments = mc.deployments.list(environment.id) except exceptions.NotFound: raise exceptions.CommandError("Environment %s not found" % args.id) else: field_labels = ["ID", "State", "Created", "Updated", "Finished"] fields = ["id", "state", "created", "updated", "finished"] utils.print_list(deployments, fields, field_labels, sortby=0)
def _get_keystone_auth(self, session, auth_url, **kwargs): auth_token = kwargs.pop('auth_token', None) if auth_token: return token.Token( auth_url, auth_token, project_id=kwargs.pop('project_id'), project_name=kwargs.pop('project_name'), project_domain_id=kwargs.pop('project_domain_id'), project_domain_name=kwargs.pop('project_domain_name')) # NOTE(starodubcevna): this is a workaround for the bug: # https://bugs.launchpad.net/python-openstackclient/+bug/1447704 # Change that fix this error in keystoneclient was abandoned, # so we should use workaround until we move to keystoneauth. # The idea of the code came from glanceclient. (v2_auth_url, v3_auth_url) = self._discover_auth_versions( session=session, auth_url=auth_url) if v3_auth_url: # NOTE(starodubcevna): set user_domain_id and project_domain_id # to default as it done in other projects. return password.Password(auth_url, username=kwargs.pop('username'), user_id=kwargs.pop('user_id'), password=kwargs.pop('password'), user_domain_id=kwargs.pop( 'user_domain_id') or 'default', user_domain_name=kwargs.pop( 'user_domain_name'), project_id=kwargs.pop('project_id'), project_name=kwargs.pop('project_name'), project_domain_id=kwargs.pop( 'project_domain_id') or 'default') elif v2_auth_url: return password.Password(auth_url, username=kwargs.pop('username'), user_id=kwargs.pop('user_id'), password=kwargs.pop('password'), project_id=kwargs.pop('project_id'), project_name=kwargs.pop('project_name')) else: # if we get here it means domain information is provided # (caller meant to use Keystone V3) but the auth url is # actually Keystone V2. Obviously we can't authenticate a V3 # user using V2. exc.CommandError("Credential and auth_url mismatch. The given " "auth_url is using Keystone V2 endpoint, which " "may not able to handle Keystone V3 credentials. " "Please provide a correct Keystone V3 auth_url.")
def do_env_template_create_env(mc, args): """Create a new environment from template.""" try: template = mc.env_templates.create_env(args.id, args.name) except common_exceptions.HTTPNotFound: raise exceptions.CommandError("Environment template %s not found" % args.id) else: formatters = { "environment_id": utils.text_wrap_formatter, "session_id": utils.text_wrap_formatter } utils.print_dict(template.to_dict(), formatters=formatters)
def do_category_delete(mc, args): """Delete a category.""" failure_count = 0 for category_id in args.id: try: mc.categories.delete(category_id) except common_exceptions.HTTPNotFound: failure_count += 1 print("Failed to delete '{0}'; category not found".format( category_id)) if failure_count == len(args.id): raise exceptions.CommandError("Unable to find and delete any of the " "specified categories.") do_category_list(mc)
def do_bundle_save(mc, args): """Save a bundle. This will download a bundle of packages with all dependencies to specified path. If path doesn't exist it will be created. """ bundle = args.filename base_url = args.murano_repo_url if args.path: if not os.path.exists(args.path): os.makedirs(args.path) dst = args.path else: dst = os.getcwd() total_reqs = collections.OrderedDict() if os.path.isfile(bundle): _file = bundle else: print( "Bundle file '{0}' does not exist, attempting to download".format( bundle)) _file = utils.to_url( bundle, base_url=base_url, path='bundles/', extension='.bundle', ) try: bundle_file = utils.Bundle.from_file(_file) except Exception as e: msg = "Failed to create bundle for {0}, reason: {1}".format(bundle, e) raise exceptions.CommandError(msg) for package in bundle_file.packages(base_url=base_url): requirements = package.requirements(base_url=base_url) total_reqs.update(requirements) no_images = getattr(args, 'no_images', False) _handle_save_packages(total_reqs, dst, base_url, no_images) try: bundle_file.save(dst, binary=False) print("Bundle file {0} has been successfully saved".format(bundle)) except Exception as e: print("Error {0} occurred while saving bundle {1}".format(e, bundle))
def do_env_template_delete(mc, args): """Delete an environment template.""" failure_count = 0 for env_template_id in args.id: try: mc.env_templates.delete(env_template_id) except common_exceptions.HTTPNotFound: failure_count += 1 mns = "Failed to delete '{0}'; environment template not found".\ format(env_template_id) if failure_count == len(args.id): raise exceptions.CommandError(mns) do_env_template_list(mc)
def generate_manifest(args): """Generates application manifest file. If some parameters are missed - they we be generated automatically. :param args: :returns: dictionary, contains manifest file data """ if not os.path.isdir(args.classes_dir): raise exceptions.CommandError( "'--classes-dir' parameter should be a directory") args = update_args(args) if not args.type: raise exceptions.CommandError( "Too few arguments: --type and --full-name is required") if not args.author: args.author = args.os_username if not args.description: args.description = "Description for the application is not provided" if not args.full_name: raise exceptions.CommandError("Please, provide --full-name parameter") manifest = { 'Format': 'MuranoPL/1.0', 'Type': args.type, 'FullName': args.full_name, 'Name': args.name, 'Description': args.description, 'Author': args.author, 'Classes': args.classes } if args.tags: manifest['Tags'] = args.tags return manifest
def do_env_template_show(mc, args): """Display environment template details.""" try: env_template = mc.env_templates.get(args.id) except common_exceptions.HTTPNotFound: raise exceptions.CommandError("Environment template %s not found" % args.id) else: formatters = { "id": utils.text_wrap_formatter, "created": utils.text_wrap_formatter, "name": utils.text_wrap_formatter, "tenant_id": utils.text_wrap_formatter, "services": utils.json_formatter, } utils.print_dict(env_template.to_dict(), formatters=formatters)
def do_environment_delete(mc, args): """Delete an environment.""" abandon = getattr(args, 'abandon', False) failure_count = 0 for environment_id in args.id: try: environment = utils.find_resource(mc.environments, environment_id) mc.environments.delete(environment.id, abandon) except exceptions.NotFound: failure_count += 1 print("Failed to delete '{0}'; environment not found".format( environment_id)) if failure_count == len(args.id): raise exceptions.CommandError("Unable to find and delete any of the " "specified environments.") do_environment_list(mc)
def check_derived_from_application(content, extends_from_application): """Look up for system 'io.murano.Application' class in extends section""" if content.get('Extends'): extends = content['Extends'] if not isinstance(extends, list): extends = [extends] for name in extends: parent_class_name = get_fqn_for_name(content.get('Namespaces'), name) if parent_class_name == 'io.murano.Application': if not extends_from_application: return True else: raise exceptions.CommandError( "Murano package should have only one class" " extends 'io.murano.Application' class") return False
def do_environment_action_call(mc, args): """Call action `ACTION` in environment `ID`. Returns id of an asynchronous task, that executes the action. Actions can only be called on a `deployed` environment. To view actions available in a given environment use `environment-show` command. """ arguments = {} for argument in args.arguments or []: if '=' not in argument: raise exceptions.CommandError( "Argument should be in form of KEY=VALUE. Found: {0}".format( argument)) k, v = argument.split('=', 1) arguments[k] = v task_id = mc.actions.call(args.id, args.action_id, arguments=arguments) print("Created task, id: {0}".format(task_id))
def do_env_template_clone(mc, args): """Create a new template, cloned from template.""" try: env_template = mc.env_templates.clone(args.id, args.name) except common_exceptions.HTTPNotFound: raise exceptions.CommandError("Environment template %s not found" % args.id) else: formatters = { "id": utils.text_wrap_formatter, "created": utils.text_wrap_formatter, "updated": utils.text_wrap_formatter, "version": utils.text_wrap_formatter, "name": utils.text_wrap_formatter, "tenant_id": utils.text_wrap_formatter, "is_public": utils.text_wrap_formatter, "services": utils.json_formatter, } utils.print_dict(env_template.to_dict(), formatters=formatters)
def do_environment_show(mc, args): """Display environment details.""" try: environment = utils.find_resource(mc.environments, args.id, session_id=args.session_id) except exceptions.NotFound: raise exceptions.CommandError("Environment %s not found" % args.id) else: if getattr(args, 'only_apps', False): print(utils.json_formatter(environment.services)) else: formatters = { "id": utils.text_wrap_formatter, "created": utils.text_wrap_formatter, "name": utils.text_wrap_formatter, "tenant_id": utils.text_wrap_formatter, "services": utils.json_formatter, } utils.print_dict(environment.to_dict(), formatters=formatters)