# Copyright (c) 2014 Rackspace # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # 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. """ Initial M1 Solum CLI commands implemented (but not REST communications): * plan create --repo="repo_url" [--build=no] plan_name * plan delete plan_name * plan list * plan show plan_id * assembly create assembly_name plan_name * assembly delete assembly_name * assembly list * assembly show assembly_id * languagepack create lp_file * languagepack list * languagepack show lp_id * languagepack delete lp_id * languagepack build lp_name git_url lp_metadata_file * component list Notes: * This code is expected to be replaced by the OpenStack Client (OSC) when it has progressed a little bit farther as described at: https://wiki.openstack.org/wiki/Solum/CLI * Internationalization will not be added in M1 since this is a prototype """ from __future__ import print_function import argparse import copy import json import sys from solumclient.common import cli_utils from solumclient.common import exc from solumclient.common import yamlutils from solumclient.openstack.common.apiclient import exceptions from solumclient.openstack.common import cliutils from solumclient.v1 import assembly as cli_assem from solumclient.v1 import pipeline as cli_pipe from solumclient.v1 import plan as cli_plan class PlanCommands(cli_utils.CommandsBase): """Commands for working with plans. Available commands: solum plan list Print an index of all available plans. solum plan show Print details about a plan. solum plan create [--param-file ] Register a plan with Solum. solum plan delete Destroy a plan. Plans with dependent assemblies cannot be deleted. """ def create(self): """Create a plan.""" self.parser.add_argument('plan_file', help="A yaml file that defines a plan," " check out solum repo for examples") self.parser.add_argument('--param-file', dest='param_file', help="A yaml file containing custom" " parameters to be used in the" " application, check out solum repo for" " examples") self.parser._names['plan_file'] = 'plan file' args, _ = self.parser.parse_known_args() try: with open(args.plan_file) as definition_file: plan_definition = definition_file.read() definition = yamlutils.load(plan_definition) except IOError: message = "Could not open plan file %s." % args.plan_file raise exc.CommandError(message=message) except ValueError: message = ("Plan file %s was not a valid YAML mapping." % args.plan_file) raise exc.CommandError(message=message) if args.param_file: try: with open(args.param_file) as param_f: param_definition = param_f.read() definition['parameters'] = yamlutils.load(param_definition) except IOError: message = "Could not open param file %s." % args.param_file raise exc.CommandError(message=message) except ValueError: message = ("Param file %s was not a valid YAML mapping." % args.param_file) raise exc.CommandError(message=message) plan = self.client.plans.create(yamlutils.dump(definition)) fields = ['uuid', 'name', 'description', 'uri', 'artifacts'] data = dict([(f, getattr(plan, f, '')) for f in fields]) artifacts = copy.deepcopy(data['artifacts']) del data['artifacts'] cliutils.print_dict(data, wrap=72) self._show_public_keys(artifacts) def delete(self): """Delete a plan.""" self.parser.add_argument('plan_uuid', help="Tenant/project-wide unique " "plan uuid or name") self.parser._names['plan_uuid'] = 'plan' args, _ = self.parser.parse_known_args() plan = self.client.plans.find(name_or_id=args.plan_uuid) cli_plan.PlanManager(self.client).delete(plan_id=str(plan.uuid)) def show(self): """Show a plan's resource.""" self.parser.add_argument('plan_uuid', help="Plan uuid or name") self.parser._names['plan_uuid'] = 'plan' args, _ = self.parser.parse_known_args() response = self.client.plans.find(name_or_id=args.plan_uuid) fields = ['uuid', 'name', 'description', 'uri', 'artifacts'] data = dict([(f, getattr(response, f, '')) for f in fields]) artifacts = copy.deepcopy(data['artifacts']) del data['artifacts'] cliutils.print_dict(data, wrap=72) self._show_public_keys(artifacts) def list(self): """List all plans.""" fields = ['uuid', 'name', 'description'] response = self.client.plans.list() cliutils.print_list(response, fields) def _show_public_keys(self, artifacts): public_keys = {} if artifacts: for arti in artifacts: if arti.content and ('public_key' in arti.content): public_keys.update( {arti.content['href']: arti.content['public_key']}) if public_keys: print('Important:') print(' Solum has generated SSH keypair for your ' + 'private github repository/ies.') print(' Please add these public SSH keys as github deploy keys.') print(' This enables solum assembly create to securely ' + 'clone/pull your private repository/ies.') print(' More details on github deploy keys: ' + 'https://developer.github.com/guides/' + 'managing-deploy-keys/#deploy-keys\n') for href, pub_key in public_keys.items(): print('%s :\n %s' % (href, pub_key)) class AssemblyCommands(cli_utils.CommandsBase): """Commands for working with assemblies. Available commands: solum assembly list Print an index of all available assemblies. solum assembly show Print the details of an assembly. solum assembly create [--description ] Create an assembly from a registered plan. solum assembly logs Print an index of all operation logs for an assembly. solum assembly delete Destroy an assembly. """ def create(self): """Create an assembly.""" self.parser.add_argument('name', help="Assembly name") self.parser.add_argument('plan_uri', help="Tenant/project-wide unique " "plan (uri/uuid or name)") self.parser.add_argument('--description', help="Assembly description") self.parser._names['plan_uri'] = 'plan URI' args, _ = self.parser.parse_known_args() name = args.name plan_uri = args.plan_uri if '/' not in plan_uri: # might be a plan uuid/name # let's try and be helpful and get the real plan_uri. plan = self.client.plans.find(name_or_id=args.plan_uri) plan_uri = plan.uri print('Note: using plan_uri=%s' % plan_uri) assembly = self.client.assemblies.create(name=name, description=args.description, plan_uri=plan_uri) fields = ['uuid', 'name', 'description', 'status', 'application_uri', 'trigger_uri'] data = dict([(f, getattr(assembly, f, '')) for f in fields]) cliutils.print_dict(data, wrap=72) def delete(self): """Delete an assembly.""" self.parser.add_argument('assembly_uuid', help="Assembly uuid or name") self.parser._names['assembly_uuid'] = 'assembly' args, _ = self.parser.parse_known_args() assem = self.client.assemblies.find(name_or_id=args.assembly_uuid) cli_assem.AssemblyManager(self.client).delete( assembly_id=str(assem.uuid)) def list(self): """List all assemblies.""" fields = ['uuid', 'name', 'description', 'status', 'created_at', 'updated_at'] response = self.client.assemblies.list() cliutils.print_list(response, fields, sortby_index=5) def logs(self): """Get Logs.""" self.parser.add_argument('assembly', help="Assembly uuid or name") args, _ = self.parser.parse_known_args() assem = self.client.assemblies.find(name_or_id=args.assembly) response = cli_assem.AssemblyManager(self.client).logs( assembly_id=str(assem.uuid)) fields = ["assembly_uuid"] for log in response: strategy_info = json.loads(log.strategy_info) if log.strategy == 'local': if 'local_storage' not in fields: fields.append('local_storage') log.local_storage = log.location elif log.strategy == 'swift': if 'swift_container' not in fields: fields.append('swift_container') if 'swift_path' not in fields: fields.append('swift_path') log.swift_container = strategy_info['container'] log.swift_path = log.location else: if 'location' not in fields: fields.append('location') cliutils.print_list(response, fields) def show(self): """Show an assembly's resource.""" self.parser.add_argument('assembly_uuid', help="Assembly uuid or name") self.parser._names['assembly_uuid'] = 'assembly' args, _ = self.parser.parse_known_args() response = self.client.assemblies.find(name_or_id=args.assembly_uuid) fields = ['uuid', 'name', 'description', 'status', 'application_uri', 'trigger_uri', 'created_at', 'updated_at'] data = dict([(f, getattr(response, f, '')) for f in fields]) cliutils.print_dict(data, wrap=72) class ComponentCommands(cli_utils.CommandsBase): """Commands for working with components. Available commands: solum component list Print an index of all available components. solum component show Print details about a component. """ def show(self): """Show a component's resource.""" self.parser.add_argument('component_uuid', help="Component uuid or name") self.parser._names['component_uuid'] = 'component' args, _ = self.parser.parse_known_args() response = self.client.components.find(name_or_id=args.component_uuid) fields = ['uuid', 'name', 'description', 'uri', 'assembly_uuid'] data = dict([(f, getattr(response, f, '')) for f in fields]) cliutils.print_dict(data, wrap=72) def list(self): """List all components.""" fields = ['uuid', 'name', 'description', 'assembly_uuid'] response = self.client.components.list() cliutils.print_list(response, fields) class PipelineCommands(cli_utils.CommandsBase): """Commands for working with pipelines. Available commands: solum pipeline list Print an index of all available pipelines. solum pipeline show Print details about a pipeline. solum pipeline create Create a pipeline from a given workbook and registered plan. solum pipeline delete Destroy a pipeline. """ def create(self): """Create a pipeline.""" self.parser.add_argument('plan_uri', help="Tenant/project-wide unique " "plan (uri/uuid or name)") self.parser.add_argument('workbook_name', help="Workbook name") self.parser.add_argument('name', help="Pipeline name") self.parser._names['plan_uri'] = 'plan URI' self.parser._names['workbook_name'] = 'workbook' args, _ = self.parser.parse_known_args() plan_uri = args.plan_uri if '/' not in plan_uri: # might be a plan uuid/name # let's try and be helpful and get the real plan_uri. plan = self.client.plans.find(name_or_id=args.plan_uri) plan_uri = plan.uri print('Note: using plan_uri=%s' % plan_uri) pipeline = self.client.pipelines.create( name=args.name, plan_uri=plan_uri, workbook_name=args.workbook_name) fields = ['uuid', 'name', 'description', 'trigger_uri'] data = dict([(f, getattr(pipeline, f, '')) for f in fields]) cliutils.print_dict(data, wrap=72) def delete(self): """Delete an pipeline.""" self.parser.add_argument('pipeline_uuid', help="Pipeline uuid or name") self.parser._names['pipeline_uuid'] = 'pipeline' args, _ = self.parser.parse_known_args() pipeline = self.client.pipelines.find(name_or_id=args.pipeline_uuid) cli_pipe.PipelineManager(self.client).delete( pipeline_id=str(pipeline.uuid)) def list(self): """List all pipelines.""" fields = ['uuid', 'name', 'description'] response = self.client.pipelines.list() cliutils.print_list(response, fields) def show(self): """Show a pipeline's resource.""" self.parser.add_argument('pipeline_uuid', help="Pipeline uuid or name") self.parser._names['pipeline_uuid'] = 'pipeline' args, _ = self.parser.parse_known_args() response = self.client.pipelines.find(name_or_id=args.pipeline_uuid) fields = ['uuid', 'name', 'description', 'trigger_uri', 'workbook_name', 'last_execution'] data = dict([(f, getattr(response, f, '')) for f in fields]) cliutils.print_dict(data, wrap=72) class LanguagePackCommands(cli_utils.CommandsBase): """Commands for working with language packs. Available commands: solum languagepack list Print and index of all available language packs. solum languagepack show Print the details of a language pack. solum languagepack create Create a new language pack from a file. solum languagepack build Create a new language pack from a git repo. solum languagepack delete Destroy a language pack. """ def create(self): """Create a language pack.""" self.parser.add_argument('lp_file', help="Language pack file.") self.parser._names['lp_file'] = 'languagepack file' args, _ = self.parser.parse_known_args() with open(args.lp_file) as lang_pack_file: try: data = json.load(lang_pack_file) except ValueError as exc: print("Error in language pack file: %s", str(exc)) sys.exit(1) languagepack = self.client.languagepacks.create(**data) fields = ['uuid', 'name', 'description', 'compiler_versions', 'os_platform'] data = dict([(f, getattr(languagepack, f, '')) for f in fields]) cliutils.print_dict(data, wrap=72) def delete(self): """Delete a language pack.""" self.parser.add_argument('lp_id', help="Language pack id") self.parser._names['lp_id'] = 'languagepack' args, _ = self.parser.parse_known_args() self.bldclient.images.delete(lp_id=args.lp_id) def list(self): """List all language packs.""" fields = ['uuid', 'name', 'description', 'state', 'source_uri'] response = self.bldclient.images.list() cliutils.print_list(response, fields) def show(self): """Get a language pack.""" self.parser.add_argument('lp_id', help="Language pack id") self.parser._names['lp_id'] = 'languagepack' args, _ = self.parser.parse_known_args() response = self.bldclient.images.find(name_or_id=args.lp_id) fields = ['uuid', 'name', 'description', 'state', 'source_uri'] data = dict([(f, getattr(response, f, '')) for f in fields]) cliutils.print_dict(data, wrap=72) def build(self): """Build a custom language pack.""" self.parser.add_argument('name', help="Language pack name.") self.parser.add_argument('git_url', help=("Github url of custom " "language pack repository.")) self.parser.add_argument('--lp_metadata', help="Language pack metadata file.") self.parser._names['git_url'] = 'repo URL' args, _ = self.parser.parse_known_args() lp_metadata = None if args.lp_metadata: with open(args.lp_metadata) as lang_pack_metadata: try: lp_metadata = json.dumps(json.load(lang_pack_metadata)) except ValueError as excp: message = ("Malformed metadata file: %s" % str(excp)) raise exc.CommandError(message=message) try: response = self.bldclient.images.create(name=args.name, source_uri=args.git_url, lp_metadata=lp_metadata) except exceptions.Conflict as conflict: message = ("%s" % conflict.message) raise exc.CommandError(message=message) fields = ['uuid', 'name', 'decription', 'state'] data = dict([(f, getattr(response, f, '')) for f in fields]) cliutils.print_dict(data, wrap=72) class AppCommands(cli_utils.CommandsBase): """Commands for working with applications. Available commands: solum app list Print an index of all deployed applications. solum app show Print detailed information about one application. solum app create [--planfile ] [--git-url ] [--langpack ] [--run-cmd ] [--name ] [--desc ] Register a new application with Solum. solum app deploy Deploy an application, building any applicable artifacts first. solum app delete Delete an application and all related artifacts. """ def _get_assemblies_by_plan(self, plan): # TODO(datsun180b): Write this. return [] def _show_public_keys(self, artifacts): # Shamelessly plucked from PlanCommands. public_keys = {} if artifacts: for arti in artifacts: if arti.content and ('public_key' in arti.content): public_keys.update( {arti.content['href']: arti.content['public_key']}) if public_keys: print('Important:') print(' Solum has generated SSH keypair for your ' + 'private github repository/ies.') print(' Please add these public SSH keys as github deploy keys.') print(' This enables solum assembly create to securely ' + 'clone/pull your private repository/ies.') print(' More details on github deploy keys: ' + 'https://developer.github.com/guides/' + 'managing-deploy-keys/#deploy-keys\n') for href, pub_key in public_keys.items(): print('%s :\n %s' % (href, pub_key)) def list(self): """Print a list of all deployed applications.""" # This is just "assembly list". # TODO(datsun180b): List each plan and its associated # assemblies. fields = ['uuid', 'name', 'description', 'status', 'created_at', 'updated_at'] assemblies = self.client.assemblies.list() cliutils.print_list(assemblies, fields, sortby_index=5) def show(self): """Print detailed information about one application.""" # This is just "plan show ". # TODO(datsun180b): List the details of the plan, and # also the current build state, build number, and running # assembly status. We don't have all the pieces for that yet. self.parser.add_argument('app', help="Application name") self.parser._names['app'] = 'application' args, _ = self.parser.parse_known_args() plan = self.client.plans.find(name_or_id=args.app) fields = ['uuid', 'name', 'description', 'uri', 'artifacts'] data = dict([(f, getattr(plan, f, '')) for f in fields]) artifacts = copy.deepcopy(data['artifacts']) del data['artifacts'] cliutils.print_dict(data, wrap=72) self._show_public_keys(artifacts) def create(self): """Register a new application with Solum.""" # This is just "plan create" with a little proactive # parsing of the planfile. self.parser.add_argument('--planfile', help="Local planfile location") self.parser.add_argument('--git-url', help='Source repo') self.parser.add_argument('--langpack', help='Language pack') self.parser.add_argument('--run-cmd', help="Application entry point") self.parser.add_argument('--name', help="Application name") self.parser.add_argument('--desc', help="Application description") self.parser.add_argument('--param-file', dest='param_file', help="A yaml file containing custom" " parameters to be used in the" " application") args, _ = self.parser.parse_known_args() # Get the plan file. Either get it from args, or supply # a skeleton. plan_definition = None if args.planfile is not None: planfile = args.planfile try: with open(planfile) as definition_file: definition = definition_file.read() plan_definition = yamlutils.load(definition) except IOError: message = "Could not open plan file %s." % planfile raise exc.CommandError(message=message) except ValueError: message = ("Plan file %s was not a valid YAML mapping." % planfile) raise exc.CommandError(message=message) else: plan_definition = { 'version': 1, 'artifacts': [{ 'artifact_type': 'heroku', 'content': {}, }]} # NOTE: This assumes the plan contains exactly one artifact. # Check for the language pack. Check args first, then planfile. # If it's neither of those places, prompt for it and update the # plan definition. langpack = None if args.langpack is not None: plan_definition['artifacts'][0]['language_pack'] = args.langpack elif plan_definition['artifacts'][0].get('language_pack') is None: langpacks = self.client.languagepacks.list() lpnames = [lp.name for lp in langpacks] fields = ['uuid', 'name', 'description', 'compiler_versions', 'os_platform'] cliutils.print_list(langpacks, fields) langpack = raw_input("Please choose a languagepack from the " "above list.\n> ") while langpack not in lpnames: langpack = raw_input("You must choose one of the named " "language packs.\n> ") plan_definition['artifacts'][0]['language_pack'] = langpack # Check for the git repo URL. Check args first, then the planfile. # If it's neither of those places, prompt for it and update the # plan definition. git_url = None if args.git_url is not None: plan_definition['artifacts'][0]['content']['href'] = args.git_url if plan_definition['artifacts'][0]['content'].get('href') is None: git_url = raw_input("Please specify a git repository URL for " "your application.\n> ") plan_definition['artifacts'][0]['content']['href'] = git_url # Check for the entry point. Check args first, then the planfile. # If it's neither of those places, prompt for it and update the # plan definition. ''' run_cmd = None if args.run_cmd is not None: plan_definition['artifacts'][0]['run_cmd'] = args.run_cmd if plan_definition['artifacts'][0].get('run_cmd') is None: run_cmd = raw_input("Please specify an entry point for your " "application.\n> ") plan_definition['artifacts'][0]['run_cmd'] = run_cmd ''' # Update name and description if specified. if args.name is not None: plan_definition['name'] = args.name if not plan_definition.get('name'): name = '' while not name: name = raw_input("Please name the application.\n> ") plan_definition['name'] = name if args.desc is not None: plan_definition['description'] = args.desc if args.param_file is not None: try: with open(args.param_file) as param_f: param_def = param_f.read() plan_definition['parameters'] = yamlutils.load(param_def) except IOError: message = "Could not open param file %s." % args.param_file raise exc.CommandError(message=message) except ValueError: message = ("Param file %s was not YAML." % args.param_file) raise exc.CommandError(message=message) plan = self.client.plans.create(yamlutils.dump(plan_definition)) fields = ['uuid', 'name', 'description', 'uri', 'artifacts'] data = dict([(f, getattr(plan, f, '')) for f in fields]) artifacts = copy.deepcopy(data['artifacts']) del data['artifacts'] cliutils.print_dict(data, wrap=72) self._show_public_keys(artifacts) def deploy(self): """Deploy an application, building any applicable artifacts first.""" # This is just "assembly create" with a little bit of introspection. # TODO(datsun180b): Add build() method, and add --build-id argument # to this method to allow for build-only and deploy-only workflows. self.parser.add_argument('app', help="Application name") self.parser._names['app'] = 'application' args, _ = self.parser.parse_known_args() plan = self.client.plans.find(name_or_id=args.app) assembly = self.client.assemblies.create(name=plan.name, description=plan.description, plan_uri=plan.uri) fields = ['uuid', 'name', 'description', 'status', 'application_uri', 'trigger_uri'] data = dict([(f, getattr(assembly, f, '')) for f in fields]) cliutils.print_dict(data, wrap=72) def delete(self): """Delete an application and all related artifacts.""" # This is "assembly delete" followed by "plan delete". self.parser.add_argument('app', help="Application name") self.parser._names['app'] = 'application' args, _ = self.parser.parse_known_args() plan = self.client.plans.find(name_or_id=args.app) assemblies = [a for a in self.client.assemblies.list() if a.plan_uri.split('/')[-1] == plan.uuid] for assembly in assemblies: assem = self.client.assemblies.find(name_or_id=assembly.uuid) cli_assem.AssemblyManager(self.client).delete( assembly_id=str(assem.uuid)) cli_plan.PlanManager(self.client).delete(plan_id=str(plan.uuid)) class PermissiveParser(argparse.ArgumentParser): """An ArgumentParser that handles errors without exiting. An argparse.ArgumentParser that doesn't sys.exit(2) when it gets the wrong number of arguments. Gives us better control over exception handling. """ # Used in _check_positional_arguments to give a clearer name to missing # positional arguments. _names = None def __init__(self, *args, **kwargs): self._names = {} kwargs['add_help'] = False kwargs['description'] = argparse.SUPPRESS kwargs['usage'] = argparse.SUPPRESS super(PermissiveParser, self).__init__(*args, **kwargs) def error(self, message): raise exc.CommandError(message=message) def _report_missing_args(self): pass def parse_known_args(self, *args, **kwargs): # Instead of sys.exit(), how about we just hand back an # empty Namespace and let someone else decide when to exit. ns, rem = argparse.Namespace(), [] try: kwargs['namespace'] = ns ns, rem = super(PermissiveParser, self).parse_known_args( *args, **kwargs) except exc.CommandError: pass self._check_positional_arguments(ns) return ns, rem def _check_positional_arguments(self, namespace): for argument in self._positionals._group_actions: argname = argument.dest localname = self._names.get(argname, argname) article = 'an' if localname[0] in 'AEIOUaeiou' else 'a' if not vars(namespace).get(argname): message = 'You must specify %(article)s %(localname)s.' message %= {'article': article, 'localname': localname} raise exc.CommandError(message=message) def main(): """Solum command-line client. For a complete description, please see README-CLI.rst. Available commands: solum help Show this help message. solum app list Print an index of all deployed applications. solum app show Print detailed information about one application. solum app create [--planfile ] [--git-url ] [--langpack ] [--run-cmd ] [--name ] [--desc ] Register a new application with Solum. solum app deploy Deploy an application, building any applicable artifacts first. solum app delete Delete an application and all related artifacts. solum plan list Print an index of all available plans. solum plan show Print details about a plan. solum plan create [--param-file ] Register a plan with Solum. solum plan delete Destroy a plan. Plans with dependent assemblies cannot be deleted. solum assembly list Print an index of all available assemblies. solum assembly create [--description ] Create an assembly from a registered plan. solum assembly delete Destroy an assembly. solum component list Print an index of all available components. solum component show Print details about a component. solum pipeline list Print an index of all available pipelines. solum pipeline show Print details about a pipeline. solum pipeline create Create a pipeline from a given workbook and registered plan. solum pipeline delete Destroy a pipeline. """ parser = PermissiveParser() resources = { 'app': AppCommands, 'plan': PlanCommands, 'assembly': AssemblyCommands, 'pipeline': PipelineCommands, 'languagepack': LanguagePackCommands, 'component': ComponentCommands, } choices = resources.keys() parser.add_argument('resource', choices=choices, default='help', help="Target noun to act upon") parsed, _ = parser.parse_known_args() resource = vars(parsed).get('resource') if resource in resources: try: resources[resource](parser) except Exception as e: print("ERROR: %s" % e.message) else: print(main.__doc__) if __name__ == '__main__': sys.exit(main())