1935 lines
75 KiB
Python
1935 lines
75 KiB
Python
# 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
|
|
* languagepack create <NAME> <GIT_REPO_URL>
|
|
* languagepack list
|
|
* languagepack show <LP_ID>
|
|
* languagepack delete <LP_ID>
|
|
* languagepack logs <LP_ID>
|
|
* 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
|
|
"""
|
|
|
|
import argparse
|
|
import copy
|
|
import json
|
|
import re
|
|
import sys
|
|
|
|
import httplib2
|
|
import jsonschema
|
|
from keystoneclient.v2_0 import client as keystoneclient
|
|
|
|
import solumclient
|
|
from solumclient.common.apiclient import exceptions
|
|
from solumclient.common import cli_utils
|
|
from solumclient.common import exc
|
|
from solumclient.common import github
|
|
from solumclient.common import yamlutils
|
|
from solumclient import config
|
|
|
|
|
|
def name_error_message(name_type):
|
|
return name_type + (" must be 1-100 characters long, only "
|
|
"contain a-z,0-9,-,_ and start with "
|
|
"an alphabet character.")
|
|
|
|
|
|
def name_is_valid(string):
|
|
if not string or not string[0].isalpha():
|
|
return False
|
|
|
|
try:
|
|
re.match(r'^([a-z0-9-_]{1,100})$', string).group(0)
|
|
except AttributeError:
|
|
return False
|
|
return True
|
|
|
|
|
|
def ValidName(string):
|
|
if not name_is_valid(string):
|
|
raise AttributeError(name_error_message("Names"))
|
|
return string
|
|
|
|
|
|
def lpname_is_valid(string):
|
|
return name_is_valid(string)
|
|
|
|
|
|
def ValidLPName(string):
|
|
if not lpname_is_valid(string):
|
|
raise AttributeError(name_error_message("LP names"))
|
|
return string
|
|
|
|
|
|
def ValidPort(string):
|
|
try:
|
|
port_val = int(string)
|
|
if 1 <= port_val <= 65535:
|
|
return port_val
|
|
else:
|
|
raise ValueError
|
|
except ValueError:
|
|
raise AttributeError("The port should be an integer between 1 and "
|
|
"65535")
|
|
|
|
|
|
def transform_git_url(git_url, private):
|
|
# try to use a correct git uri
|
|
pt = re.compile(r'github\.com[:/](.+?)/(.+?)($|/.*$|\.git$|\.git/.*$)')
|
|
match = pt.search(git_url)
|
|
if match:
|
|
user_org_name = match.group(1)
|
|
repo = match.group(2)
|
|
if private:
|
|
right_uri = 'git@github.com:%s/%s.git' % (user_org_name, repo)
|
|
else:
|
|
right_uri = 'https://github.com/%s/%s.git' % (user_org_name, repo)
|
|
return right_uri
|
|
else:
|
|
msg = "Provide the git uri in the following format: "
|
|
if not private:
|
|
msg = msg + "https://github.com/<USER>/<REPO>.git"
|
|
else:
|
|
msg = msg + "git@github.com:<USER>/<REPO>.git"
|
|
raise exc.CommandError(message=msg)
|
|
|
|
|
|
def show_public_keys(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 and uploaded SSH keypair for your ' +
|
|
'private github repository/ies.')
|
|
print(' You may need to add these public SSH keys as github ' +
|
|
'deploy keys, if they were not uploaded successfully.')
|
|
print(' This enables solum 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 PlanCommands(cli_utils.CommandsBase):
|
|
"""Commands for working with plans.
|
|
|
|
Available commands:
|
|
|
|
solum plan list
|
|
Print an index of all available plans.
|
|
|
|
solum plan show <PLAN>
|
|
Print details about a plan.
|
|
|
|
solum plan create <PLANFILE> [--param-file <PARAMFILE>]
|
|
Register a plan with Solum.
|
|
|
|
solum plan delete <PLAN>
|
|
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_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']
|
|
artifacts = copy.deepcopy(vars(plan).get('artifacts'))
|
|
self._print_dict(plan, fields, wrap=72)
|
|
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_args()
|
|
plan = self.client.plans.find(name_or_id=args.plan_uuid)
|
|
self.client.plans.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_args()
|
|
plan = self.client.plans.find(name_or_id=args.plan_uuid)
|
|
fields = ['uuid', 'name', 'description', 'uri', 'artifacts']
|
|
artifacts = copy.deepcopy(vars(plan).get('artifacts'))
|
|
self._print_dict(plan, fields, wrap=72)
|
|
show_public_keys(artifacts)
|
|
|
|
def list(self):
|
|
"""List all plans."""
|
|
fields = ['uuid', 'name', 'description']
|
|
plans = self.client.plans.list()
|
|
self._print_list(plans, fields)
|
|
|
|
|
|
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 <UUID>
|
|
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_args()
|
|
component = self.client.components.find(name_or_id=args.component_uuid)
|
|
fields = ['uuid', 'name', 'description', 'uri', 'assembly_uuid']
|
|
self._print_dict(component, fields, wrap=72)
|
|
|
|
def list(self):
|
|
"""List all components."""
|
|
fields = ['uuid', 'name', 'description', 'assembly_uuid']
|
|
components = self.client.components.list()
|
|
self._print_list(components, 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 <PIPELINE>
|
|
Print details about a pipeline.
|
|
|
|
solum pipeline create <PLAN_URI> <WORKBOOK_NAME> <NAME>
|
|
Create a pipeline from a given workbook and registered plan.
|
|
|
|
solum pipeline delete <PIPELINE>
|
|
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',
|
|
type=ValidName,
|
|
help="Pipeline name")
|
|
self.parser._names['plan_uri'] = 'plan URI'
|
|
self.parser._names['workbook_name'] = 'workbook'
|
|
args = self.parser.parse_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']
|
|
self._print_dict(pipeline, fields, 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_args()
|
|
pipeline = self.client.pipelines.find(name_or_id=args.pipeline_uuid)
|
|
self.client.pipelines.delete(pipeline_id=str(pipeline.uuid))
|
|
|
|
def list(self):
|
|
"""List all pipelines."""
|
|
fields = ['uuid', 'name', 'description']
|
|
pipelines = self.client.pipelines.list()
|
|
self._print_list(pipelines, 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_args()
|
|
pipelines = self.client.pipelines.find(name_or_id=args.pipeline_uuid)
|
|
fields = ['uuid', 'name', 'description',
|
|
'trigger_uri', 'workbook_name', 'last_execution']
|
|
self._print_dict(pipelines, fields, wrap=72)
|
|
|
|
|
|
class LanguagePackCommands(cli_utils.CommandsBase):
|
|
"""Commands for working with language packs.
|
|
|
|
Available commands:
|
|
|
|
solum lp create <NAME> <GIT_REPO_URL> [--param-file <param-file-name>]
|
|
Create a new language pack from a git repo.
|
|
You can pass an optional parameter file containing a dictionary.
|
|
One use of the parameter file is to pass in credentials for
|
|
infrastructure that should be used to build the lp.
|
|
|
|
solum lp list
|
|
Print and index of all available language packs.
|
|
|
|
solum lp show <NAME|UUID>
|
|
Print the details of a language pack.
|
|
|
|
solum lp delete <NAME|UUID>
|
|
Destroy a language pack.
|
|
|
|
solum lp logs <NAME|UUID>
|
|
Show logs for a language pack.
|
|
"""
|
|
|
|
def create(self):
|
|
"""Create a language pack."""
|
|
self.parser.add_argument('--param-file',
|
|
dest='paramfile',
|
|
help="Local parameter file location")
|
|
self.parser.add_argument('name',
|
|
type=ValidLPName,
|
|
help="Language pack name.")
|
|
self.parser.add_argument('--name',
|
|
type=ValidLPName,
|
|
dest='name',
|
|
help="Language pack name.")
|
|
self.parser.add_argument('git_url',
|
|
help=("Github url of custom "
|
|
"language pack repository."))
|
|
self.parser.add_argument('--git_url',
|
|
dest='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_args()
|
|
|
|
param_data = {}
|
|
if args.paramfile is not None:
|
|
try:
|
|
with open(args.paramfile, 'r') as inf:
|
|
param_data = yamlutils.load(inf.read())
|
|
except Exception as exp:
|
|
raise exc.CommandException(str(exp))
|
|
|
|
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)
|
|
|
|
languagepack = {}
|
|
try:
|
|
languagepack = self.client.languagepacks.create(
|
|
name=args.name, source_uri=args.git_url,
|
|
lp_metadata=lp_metadata,
|
|
lp_params=param_data)
|
|
except exceptions.Conflict as conflict:
|
|
message = ("%s" % conflict.message)
|
|
raise exc.CommandError(message=message)
|
|
|
|
fields = ['uuid', 'name', 'description', 'status', 'source_uri']
|
|
self._print_dict(languagepack, fields, 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_args()
|
|
self.client.languagepacks.delete(lp_id=args.lp_id)
|
|
|
|
def list(self):
|
|
"""List all language packs."""
|
|
fields = ['uuid', 'name', 'description', 'status', 'source_uri']
|
|
languagepacks = self.client.languagepacks.list()
|
|
self._print_list(languagepacks, 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_args()
|
|
languagepack = self.client.languagepacks.find(name_or_id=args.lp_id)
|
|
fields = ['uuid', 'name', 'description', 'status', 'source_uri']
|
|
self._print_dict(languagepack, fields, wrap=72)
|
|
|
|
def logs(self):
|
|
"""Get language pack Logs."""
|
|
self.parser.add_argument('lp_id',
|
|
help="languagepack uuid or name")
|
|
args = self.parser.parse_args()
|
|
loglist = self.client.languagepacks.logs(lp_id=str(args.lp_id))
|
|
|
|
fields = ["resource_uuid", "created_at"]
|
|
for log in loglist:
|
|
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')
|
|
|
|
self._print_list(loglist, fields)
|
|
|
|
|
|
class AppCommands(cli_utils.CommandsBase):
|
|
"""Commands for working with actual applications.
|
|
|
|
Available commands:
|
|
solum app list
|
|
Print an index of all deployed applications.
|
|
|
|
solum app show <NAME|ID>
|
|
Print detailed information about one application.
|
|
|
|
solum app create [--app-file <AppFile>] [--git-url <GIT_URL>]
|
|
[--lp <LANGUAGEPACK>]
|
|
[--param-file <PARAMFILE>]
|
|
[--setup-trigger]
|
|
[--port <PORT>]
|
|
[--private-repo]
|
|
[--no-languagepack]
|
|
[--trigger-workflow <CUSTOM-WORKFLOW>]
|
|
<CUSTOM-WORKFLOW>=(unittest | build | unittest+build)
|
|
Without the --trigger-workflow flag,
|
|
the workflow unittest+build+deploy is triggered
|
|
(this is the default workflow)
|
|
|
|
Register a new application with Solum.
|
|
|
|
solum app deploy <NAME|ID> --du-id <du-id>
|
|
Deploy an application, building any applicable artifacts first.
|
|
du-id is optional flag. It can be used to pass in id of a previously
|
|
created deployment unit. If passed, this command will deploy that
|
|
du instead of building one first.
|
|
|
|
solum app delete <NAME|ID>
|
|
Delete an application and all related artifacts.
|
|
|
|
solum app scale <NAME|ID> <scaling target>
|
|
|
|
solum app logs <NAME|UUID> [--wf-id <wf-id>]
|
|
Show the logs of an application for all the workflows.
|
|
wf-id is optional flag which can be used to pass in id of one of
|
|
the existing workflows. If provided, the logs only for that workflow
|
|
are displayed.
|
|
"""
|
|
|
|
def _validate_app_file(self, app_data):
|
|
if ('workflow_config' in app_data and
|
|
app_data.get('workflow_config') is None):
|
|
msg = "Workflow config cannot be empty"
|
|
raise exc.CommandException(message=msg)
|
|
if ('trigger_actions' in app_data and
|
|
app_data.get('trigger_actions') is None):
|
|
msg = "Trigger actions cannot be empty"
|
|
raise exc.CommandException(message=msg)
|
|
|
|
error_message = ("Application name must be 1-100 characters long, "
|
|
"only contain a-z,0-9,-,_ and start with an alphabet "
|
|
"character.")
|
|
|
|
if app_data.get('name') is not None:
|
|
if not name_is_valid(app_data.get('name')):
|
|
raise exc.CommandError(message=error_message)
|
|
|
|
if 'repo_token' not in app_data:
|
|
app_data['repo_token'] = ''
|
|
|
|
def _get_and_validate_app_name(self, app_data, args):
|
|
# Check the appfile-supplied name first.
|
|
error_message = ("Application name must be 1-100 characters long, "
|
|
"only contain a-z,0-9,-,_ and start with an alphabet "
|
|
"character.")
|
|
app_name = ''
|
|
if args.name:
|
|
app_name = args.name
|
|
elif app_data.get('name') is None:
|
|
while True:
|
|
app_name = input("Please name the application.\n> ")
|
|
if name_is_valid(app_name):
|
|
break
|
|
print(error_message)
|
|
else:
|
|
app_name = app_data.get('name')
|
|
|
|
if not name_is_valid(app_name):
|
|
raise exc.CommandError(message=error_message)
|
|
|
|
return app_name
|
|
|
|
def _get_and_validate_languagepack(self, app_data, args):
|
|
# Check for the language pack. Check args first, then appfile.
|
|
# If it's neither of those places, prompt for it and update the
|
|
# app data.
|
|
|
|
if args.no_languagepack:
|
|
app_data['languagepack'] = "False"
|
|
return
|
|
|
|
languagepack = None
|
|
if args.languagepack is not None:
|
|
languagepack = args.languagepack
|
|
elif app_data.get('languagepack') is not None:
|
|
languagepack = app_data.get('languagepack')
|
|
# check if given languagepack exists or not
|
|
if languagepack:
|
|
try:
|
|
lp = self.client.languagepacks.find(name_or_id=languagepack)
|
|
except Exception:
|
|
raise exc.CommandError(
|
|
"Languagepack '%s' not found." % languagepack)
|
|
if lp is None or lp.status != 'READY':
|
|
raise exc.CommandError("No languagepack in READY state. "
|
|
"Create a languagepack first.")
|
|
app_data['languagepack'] = languagepack
|
|
else:
|
|
languagepacks = self.client.languagepacks.list()
|
|
filtered_list = cli_utils.filter_ready_lps(languagepacks)
|
|
|
|
if len(filtered_list) > 0:
|
|
lpnames = [lang_pack.name for lang_pack in filtered_list]
|
|
lp_uuids = [lang_pack.uuid for lang_pack in filtered_list]
|
|
fields = ['uuid', 'name', 'description',
|
|
'status', 'source_uri']
|
|
self._print_list(filtered_list, fields)
|
|
languagepack = input("Please choose a languagepack "
|
|
"from the above list.\n> ")
|
|
while languagepack not in lpnames + lp_uuids:
|
|
languagepack = input("You must choose one of "
|
|
"the named language "
|
|
"packs.\n> ")
|
|
app_data['languagepack'] = languagepack
|
|
else:
|
|
raise exc.CommandError("No languagepack in READY state. "
|
|
"Create a languagepack first.")
|
|
|
|
def _get_app_repo_details(self, app_data, args):
|
|
def read_private_sshkey(sshkey_file):
|
|
private_sshkey = ''
|
|
try:
|
|
with open(sshkey_file, 'r') as inf:
|
|
private_sshkey = inf.read()
|
|
except Exception as exp:
|
|
raise exc.CommandException(str(exp))
|
|
return private_sshkey
|
|
|
|
git_rev = 'master'
|
|
git_url = None
|
|
if (app_data.get('source') is not None and
|
|
app_data.get('source').get('repository') is not None):
|
|
git_url = app_data['source']['repository']
|
|
# Commandline flag overrides stuff in the app-file
|
|
if args.git_url is not None:
|
|
git_url = args.git_url
|
|
# Take input from user
|
|
elif (app_data.get('source') is None or
|
|
app_data['source'].get('repository') is None or
|
|
app_data['source']['repository'] == ''):
|
|
git_url = input("Please specify a git repository URL "
|
|
"for your application.\n> ")
|
|
git_rev_i = input("Please specify revision"
|
|
"(default is master).\n> ")
|
|
if git_rev_i == '':
|
|
git_rev = 'master'
|
|
else:
|
|
git_rev = git_rev_i
|
|
|
|
assert(git_url is not None)
|
|
assert(git_rev is not None)
|
|
|
|
# check if given repo is a private repository
|
|
is_private = (args.private_repo or
|
|
app_data['source'].get('private', False))
|
|
|
|
git_url = transform_git_url(git_url, is_private)
|
|
|
|
private_sshkey = app_data['source'].get('private_ssh_key', '')
|
|
if is_private and not private_sshkey:
|
|
sshkey_file = input("Please specify private sshkey file "
|
|
"full path: ")
|
|
sshkey_file = sshkey_file.strip()
|
|
private_sshkey = read_private_sshkey(sshkey_file)
|
|
|
|
if is_private and private_sshkey == '':
|
|
msg = "Must provide private sshkey for private repositories."
|
|
raise exc.CommandError(message=msg)
|
|
|
|
git_src = dict()
|
|
git_src['private'] = is_private
|
|
git_src['private_ssh_key'] = private_sshkey
|
|
git_src['repository'] = git_url
|
|
git_src['revision'] = git_rev
|
|
app_data['source'] = git_src
|
|
|
|
def _get_run_command(self, app_data, args):
|
|
run_cmd = None
|
|
|
|
if args.run_cmd is not None:
|
|
run_cmd = args.run_cmd
|
|
elif (app_data.get('workflow_config') is None or
|
|
app_data['workflow_config'].get('run_cmd') == '' or
|
|
app_data['workflow_config'].get('run_cmd') is None):
|
|
run_cmd = input("Please specify start/run command for "
|
|
"your application.\n> ")
|
|
|
|
if app_data.get('workflow_config') is None:
|
|
run_cmd_dict = dict()
|
|
run_cmd_dict['run_cmd'] = run_cmd
|
|
app_data['workflow_config'] = run_cmd_dict
|
|
elif (app_data['workflow_config'].get('run_cmd') == '' or
|
|
app_data['workflow_config'].get('run_cmd') is None):
|
|
app_data['workflow_config']['run_cmd'] = run_cmd
|
|
|
|
def _get_unittest_command(self, app_data, args):
|
|
unittest_cmd = None
|
|
|
|
if args.unittest_cmd is not None:
|
|
unittest_cmd = args.unittest_cmd
|
|
if app_data.get('workflow_config') is None:
|
|
unittest_cmd_dict = dict()
|
|
unittest_cmd_dict['test_cmd'] = unittest_cmd
|
|
app_data['workflow_config'] = unittest_cmd_dict
|
|
elif (app_data['workflow_config'].get('test_cmd') == '' or
|
|
app_data['workflow_config'].get('test_cmd') is None):
|
|
app_data['workflow_config']['test_cmd'] = unittest_cmd
|
|
|
|
def _get_port(self, app_data, args):
|
|
port_list = []
|
|
|
|
if (app_data.get('ports') is None or
|
|
app_data['ports'] == '' or app_data['ports'] == [None]):
|
|
if args.port:
|
|
port_list.append(int(args.port))
|
|
else:
|
|
print("Using 80 as the app's default listening port")
|
|
port_list.append(int(80))
|
|
|
|
app_data['ports'] = port_list
|
|
|
|
def _get_parameters(self, app_data, args):
|
|
app_data['parameters'] = {}
|
|
if args.param_file is not None:
|
|
try:
|
|
with open(args.param_file) as param_f:
|
|
param_def = param_f.read()
|
|
app_data['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)
|
|
|
|
def _setup_github_trigger(self, app_data, app, args):
|
|
# If a token is supplied, we won't need to generate one.
|
|
provided_token = ''
|
|
obtained_token = ''
|
|
if 'repo_token' in app_data:
|
|
provided_token = app_data['repo_token']
|
|
|
|
if args.setup_trigger or args.workflow:
|
|
trigger_uri = vars(app).get('trigger_uri', '')
|
|
if trigger_uri:
|
|
workflow = None
|
|
if args.workflow:
|
|
workflow = args.workflow.replace('+', ' ').split(' ')
|
|
try:
|
|
git_url = app_data['source']['repository']
|
|
|
|
gha = github.GitHubAuth(git_url,
|
|
repo_token=provided_token)
|
|
gha.create_webhook(trigger_uri, workflow=workflow)
|
|
|
|
if not provided_token:
|
|
try:
|
|
obtained_token = gha.create_repo_token()
|
|
# Update the app with the generated repo_token
|
|
to_update = {}
|
|
to_update['repo_token'] = obtained_token
|
|
self.client.apps.patch(app_id=app.id, **to_update)
|
|
except Exception:
|
|
print("Error in obtaining github token.")
|
|
return ''
|
|
|
|
except github.GitHubException as ghe:
|
|
raise exc.CommandError(message=str(ghe))
|
|
|
|
def create(self):
|
|
|
|
parsed, _ = self.parser.parse_known_args()
|
|
config.username = parsed.os_username
|
|
config.password = parsed.os_password
|
|
config.tenant = parsed.os_tenant_name
|
|
|
|
self.register()
|
|
|
|
def register(self):
|
|
"""Register a new app."""
|
|
self.parser.add_argument('--app-file',
|
|
dest='appfile',
|
|
help="Local appfile location")
|
|
self.parser.add_argument('--name',
|
|
dest='name',
|
|
type=ValidName,
|
|
help="Application name")
|
|
self.parser.add_argument('--languagepack',
|
|
help='Language pack')
|
|
self.parser.add_argument('--lp',
|
|
dest='languagepack',
|
|
help='Language pack')
|
|
self.parser.add_argument('--git-url',
|
|
dest='git_url',
|
|
help='Source repo')
|
|
self.parser.add_argument('--run-cmd',
|
|
dest='run_cmd',
|
|
help="Application entry point")
|
|
self.parser.add_argument('--unittest-cmd',
|
|
dest='unittest_cmd',
|
|
help="Command to execute unit tests")
|
|
self.parser.add_argument('--port',
|
|
dest="port",
|
|
type=ValidPort,
|
|
help="The port your application listens on")
|
|
self.parser.add_argument('--param-file',
|
|
dest='param_file',
|
|
help="A yaml file containing custom"
|
|
" parameters to be used in the"
|
|
" application")
|
|
self.parser.add_argument('--setup-trigger',
|
|
action='store_true',
|
|
dest='setup_trigger',
|
|
help="Set up app trigger on git repo")
|
|
self.parser.add_argument('--no-languagepack',
|
|
action='store_true',
|
|
dest='no_languagepack',
|
|
help="Flag to register an app without"
|
|
" a languagepack")
|
|
self.parser.add_argument('--private-repo',
|
|
action='store_true',
|
|
dest='private_repo',
|
|
help="Source repo requires authentication.")
|
|
|
|
trigger_help = ("Which of stages build, unittest, deploy to trigger "
|
|
"from git. For example: "
|
|
"--trigger-workflow=unittest+build+deploy. "
|
|
"Implies --setup-trigger.")
|
|
self.parser.add_argument('--trigger-workflow',
|
|
default='',
|
|
dest='workflow',
|
|
help=trigger_help)
|
|
|
|
args = self.parser.parse_args()
|
|
app_data = None
|
|
if args.appfile is not None:
|
|
try:
|
|
with open(args.appfile, 'r') as inf:
|
|
app_data = yamlutils.load(inf.read())
|
|
self._validate_app_file(app_data)
|
|
except Exception as exp:
|
|
raise exc.CommandException(str(exp))
|
|
else:
|
|
app_data = {
|
|
'version': 1,
|
|
'description': 'default app description.',
|
|
'source': {
|
|
'repository': '',
|
|
'revision': 'master',
|
|
'private': False,
|
|
'private_ssh_key': '',
|
|
'repo_token': ''
|
|
},
|
|
'workflow_config': {
|
|
'test_cmd': '',
|
|
'run_cmd': ''
|
|
},
|
|
'trigger_actions': ['build', 'deploy'],
|
|
}
|
|
|
|
app_name = self._get_and_validate_app_name(app_data, args)
|
|
app_data['name'] = app_name
|
|
|
|
self._get_and_validate_languagepack(app_data, args)
|
|
|
|
self._get_app_repo_details(app_data, args)
|
|
|
|
self._get_run_command(app_data, args)
|
|
|
|
self._get_unittest_command(app_data, args)
|
|
|
|
self._get_port(app_data, args)
|
|
|
|
self._get_parameters(app_data, args)
|
|
|
|
self._validate_app_data(app_data)
|
|
|
|
if args.workflow:
|
|
app_data['trigger_actions'] = (
|
|
args.workflow.replace('+', ' ').split(' '))
|
|
|
|
app = self.client.apps.create(**app_data)
|
|
|
|
self._setup_github_trigger(app_data, app, args)
|
|
|
|
app.trigger = app.trigger_actions
|
|
app.workflow = app.workflow_config
|
|
|
|
fields = ['name', 'id', 'created_at', 'description', 'languagepack',
|
|
'ports', 'source', 'workflow',
|
|
'trigger_uuid', 'trigger', 'trigger_uri']
|
|
self._print_dict(app, fields, wrap=72)
|
|
|
|
def _validate_app_data(self, app_data):
|
|
# app file schema
|
|
schema = {
|
|
"title": "app file schema",
|
|
"type": "object",
|
|
"properties": {
|
|
"version": {
|
|
"type": "integer"
|
|
},
|
|
"name": {
|
|
"type": "string"
|
|
},
|
|
"description": {
|
|
"type": "string"
|
|
},
|
|
"languagepack": {
|
|
"type": "string"
|
|
},
|
|
"source": {
|
|
"type": "object",
|
|
"properties": {
|
|
"repository": {
|
|
"type": "string"
|
|
},
|
|
"revision": {
|
|
"type": "string"
|
|
},
|
|
"private": {
|
|
"type": "boolean"
|
|
},
|
|
"repo_token": {
|
|
"type": "string"
|
|
},
|
|
"private_ssh_key": {
|
|
"type": "string"
|
|
}
|
|
}
|
|
},
|
|
"trigger_actions": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string",
|
|
"enum": ["unittest", "build", "deploy"]
|
|
}
|
|
},
|
|
"workflow_config": {
|
|
"type": "object",
|
|
"properties": {
|
|
"test_cmd": {
|
|
"type": "string"
|
|
},
|
|
"run_cmd": {
|
|
"type": "string"
|
|
}
|
|
}
|
|
},
|
|
"ports": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "integer"
|
|
}
|
|
},
|
|
"parameters": {
|
|
"type": "object"
|
|
}
|
|
},
|
|
"required": [
|
|
"version",
|
|
"name",
|
|
"description",
|
|
"languagepack",
|
|
"source",
|
|
"trigger_actions",
|
|
"workflow_config",
|
|
"ports"
|
|
]
|
|
}
|
|
try:
|
|
jsonschema.validate(app_data, schema)
|
|
except jsonschema.exceptions.ValidationError as exp:
|
|
raise exc.CommandError(message=str(exp))
|
|
|
|
def update(self):
|
|
"""Update the registration of an existing app."""
|
|
self.parser.add_argument('app')
|
|
|
|
self.parser.add_argument('--name', type=str)
|
|
self.parser.add_argument('--desc', type=str)
|
|
self.parser.add_argument('--lp', type=str)
|
|
self.parser.add_argument('--ports', type=str)
|
|
self.parser.add_argument('--source.repo', dest='source_repo', type=str)
|
|
self.parser.add_argument('--source.rev', dest='source_rev', type=str)
|
|
self.parser.add_argument('--test_cmd', type=str)
|
|
self.parser.add_argument('--run_cmd', type=str)
|
|
self.parser.add_argument('--trigger', type=str)
|
|
|
|
args = self.parser.parse_args()
|
|
|
|
app = self.client.apps.find(name_or_id=args.app)
|
|
|
|
to_update = {}
|
|
if args.name:
|
|
to_update['name'] = args.name
|
|
if args.desc:
|
|
to_update['description'] = args.desc
|
|
if args.lp:
|
|
to_update['languagepack'] = args.lp
|
|
if args.ports:
|
|
ports = args.ports.strip('[]').replace(',', ' ')
|
|
ports = [int(p, 10) for p in ports.split(' ') if p]
|
|
to_update['ports'] = ports
|
|
if args.source_repo:
|
|
to_update['source'] = to_update.get('source', {})
|
|
to_update['source']['repository'] = args.source_repo
|
|
if args.source_rev:
|
|
to_update['source'] = to_update.get('source', {})
|
|
to_update['source']['revision'] = args.source_rev
|
|
if args.test_cmd:
|
|
to_update['workflow_config'] = to_update.get('trigger', {})
|
|
to_update['workflow_config']['test_cmd'] = args.test_cmd
|
|
if args.run_cmd:
|
|
to_update['workflow_config'] = to_update.get('trigger', {})
|
|
to_update['workflow_config']['run_cmd'] = args.run_cmd
|
|
if args.trigger:
|
|
trigger = args.trigger.strip('[]').replace(',', ' ').split(' ')
|
|
to_update['trigger_actions'] = trigger
|
|
|
|
if not to_update:
|
|
raise exc.CommandException(message="Nothing to update")
|
|
|
|
updated_app = self.client.apps.patch(app_id=app.id, **to_update)
|
|
|
|
updated_app.trigger = updated_app.trigger_actions
|
|
updated_app.workflow = updated_app.workflow_config
|
|
|
|
fields = ['name', 'id', 'created_at', 'description', 'languagepack',
|
|
'entry_points', 'ports', 'source', 'workflow',
|
|
'trigger_uuid', 'trigger']
|
|
self._print_dict(updated_app, fields, wrap=72)
|
|
|
|
def list(self):
|
|
"""List all apps."""
|
|
apps = self.client.apps.list()
|
|
fields = ['name', 'id', 'created_at', 'description', 'languagepack']
|
|
self._print_list(apps, fields)
|
|
|
|
def show(self):
|
|
"""Show details of one app."""
|
|
self.parser.add_argument('name')
|
|
args = self.parser.parse_args()
|
|
app = self.client.apps.find(name_or_id=args.name)
|
|
|
|
fields = ['name', 'id', 'created_at', 'updated_at', 'description',
|
|
'languagepack', 'entry_points', 'ports', 'source',
|
|
'trigger_uuid', 'trigger', 'app_url']
|
|
|
|
app.trigger = app.trigger_actions
|
|
app.workflow = app.workflow_config
|
|
if app.scale_config:
|
|
if app.scale_config[app.name]['target']:
|
|
app.target_instances = app.scale_config[app.name]['target']
|
|
fields.append('target_instances')
|
|
|
|
self._print_dict(app, fields, wrap=72)
|
|
|
|
wfs = self.client.workflows.list(app_id=app.id)
|
|
fields = ['wf_id', 'id', 'status']
|
|
print("'%s' workflows and their status:" % args.name)
|
|
self._print_list(wfs, fields)
|
|
|
|
def delete(self):
|
|
"""Delete an app."""
|
|
self.parser.add_argument('name')
|
|
args = self.parser.parse_args()
|
|
app = self.client.apps.find(name_or_id=args.name)
|
|
self.client.apps.delete(app_id=str(app.id))
|
|
|
|
def _create_scaling_workflow(self, actions, app_name_id, tgt):
|
|
app = self.client.apps.find(name_or_id=app_name_id)
|
|
kwargs = {'app_id': app.id, 'actions': actions,
|
|
'scale_target': tgt}
|
|
wf = self.client.workflows.create(**kwargs)
|
|
fields = ['wf_id', 'app_id', 'actions', 'config',
|
|
'source', 'id', 'created_at', 'updated_at']
|
|
self._print_dict(wf, fields, wrap=72)
|
|
|
|
def _create_workflow(self, actions, app_name_id=''):
|
|
if not app_name_id:
|
|
app = self.client.apps.find(name_or_id=app_name_id)
|
|
else:
|
|
self.parser.add_argument('name')
|
|
args = self.parser.parse_args()
|
|
app = self.client.apps.find(name_or_id=args.name)
|
|
kwargs = {'app_id': app.id, 'actions': actions}
|
|
wf = self.client.workflows.create(**kwargs)
|
|
fields = ['wf_id', 'app_id', 'actions', 'config',
|
|
'source', 'id', 'created_at', 'updated_at']
|
|
self._print_dict(wf, fields, wrap=72)
|
|
|
|
def _create_workflow_for_prebuilt_du(self, actions, app_name_id, du_id):
|
|
app = self.client.apps.find(name_or_id=app_name_id)
|
|
kwargs = {'app_id': app.id, 'actions': actions,
|
|
'du_id': du_id}
|
|
wf = self.client.workflows.create(**kwargs)
|
|
fields = ['wf_id', 'app_id', 'actions', 'config',
|
|
'source', 'id', 'created_at', 'updated_at']
|
|
self._print_dict(wf, fields, wrap=72)
|
|
|
|
def unittest(self):
|
|
"""Create a new workflow for an app."""
|
|
actions = ['unittest']
|
|
self._create_workflow(actions)
|
|
|
|
def build(self):
|
|
"""Create a new workflow for an app."""
|
|
actions = ['unittest', 'build']
|
|
self._create_workflow(actions)
|
|
|
|
def deploy(self):
|
|
"""Create a new workflow for an app."""
|
|
self.parser.add_argument("name")
|
|
self.parser.add_argument('--du-id',
|
|
dest="du_id",
|
|
help="ID of the DU image.")
|
|
args = self.parser.parse_args()
|
|
if args.du_id:
|
|
actions = ['deploy']
|
|
self._create_workflow_for_prebuilt_du(actions, args.name,
|
|
args.du_id)
|
|
else:
|
|
actions = ['unittest', 'build', 'deploy']
|
|
self._create_workflow(actions, args.name)
|
|
|
|
def scale(self):
|
|
"""Scale the app."""
|
|
self.parser.add_argument('name')
|
|
self.parser.add_argument('target')
|
|
args = self.parser.parse_args()
|
|
|
|
target = args.target
|
|
try:
|
|
target = int(target)
|
|
except ValueError:
|
|
msg = "Must provide integer value for scale target."
|
|
raise exc.CommandException(message=msg)
|
|
|
|
if target <= 0:
|
|
msg = "Scale target must be greater than zero."
|
|
raise exc.CommandException(message=msg)
|
|
|
|
actions = ['scale']
|
|
self._create_scaling_workflow(actions, args.name, target)
|
|
|
|
def _display_logs_for_all_workflows(self, app):
|
|
wfs = self.client.workflows.list(app_id=app.id)
|
|
|
|
all_logs_list = []
|
|
fields = ["resource_uuid", "created_at"]
|
|
for wf in wfs:
|
|
revision = wf.wf_id
|
|
kwargs = {'app_id': app.id, 'revision_or_id': revision}
|
|
loglist = self.client.workflows.logs(**kwargs)
|
|
for log in loglist:
|
|
all_logs_list.append(log)
|
|
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')
|
|
self._print_list(all_logs_list, fields)
|
|
|
|
def logs(self):
|
|
"""Print a list of all logs belonging to a single app."""
|
|
|
|
self.parser.add_argument('name')
|
|
|
|
self.parser.add_argument('--wf-id',
|
|
dest='wf_id',
|
|
help="Workflow ID")
|
|
|
|
args = self.parser.parse_args()
|
|
app = self.client.apps.find(name_or_id=args.name)
|
|
|
|
if args.wf_id:
|
|
try:
|
|
revision = int(args.wf_id, 10)
|
|
except (ValueError, TypeError):
|
|
revision = args.wf_id
|
|
display_logs_for_single_workflow(self, app, revision)
|
|
else:
|
|
self._display_logs_for_all_workflows(app)
|
|
|
|
|
|
def display_logs_for_single_workflow(self, app, revision):
|
|
kwargs = {'app_id': app.id, 'revision_or_id': revision}
|
|
loglist = self.client.workflows.logs(**kwargs)
|
|
fields = ["resource_uuid", "created_at"]
|
|
for log in loglist:
|
|
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')
|
|
|
|
self._print_list(loglist, fields)
|
|
|
|
|
|
class WorkflowCommands(cli_utils.CommandsBase):
|
|
"""Commands for working with workflows.
|
|
|
|
Available commands:
|
|
|
|
solum workflow list <APP_NAME|UUID>
|
|
List all application workflows.
|
|
|
|
solum workflow show <APP_NAME|UUID> <WORKFLOW_ID|UUID>
|
|
Print the details of a workflow.
|
|
|
|
solum workflow logs <APP_NAME|UUID> <WORKFLOW_ID|UUID>
|
|
List all the logs of a given workflow.
|
|
"""
|
|
|
|
def list(self):
|
|
"""Show all of an app's live workflows."""
|
|
self.parser.add_argument('app')
|
|
args = self.parser.parse_args()
|
|
app = self.client.apps.find(name_or_id=args.app)
|
|
wfs = self.client.workflows.list(app_id=app.id)
|
|
fields = ['wf_id', 'id', 'actiONs', 'status',
|
|
'created_at', 'updated_at']
|
|
self._print_list(wfs, fields)
|
|
|
|
def show(self):
|
|
"""Show one of an app's live workflows."""
|
|
# Either "solum workflow show <app_id_or_name> <workflow_uuid>
|
|
# Or "solum workflow show <app_id_or_name> <workflow_revision>
|
|
self.parser.add_argument('app')
|
|
self.parser.add_argument('workflow')
|
|
args = self.parser.parse_args()
|
|
revision = args.workflow
|
|
try:
|
|
revision = int(revision, 10)
|
|
except ValueError:
|
|
revision = args.workflow
|
|
app = self.client.apps.find(name_or_id=args.app)
|
|
|
|
kwargs = {'app_id': app.id, 'revision_or_id': revision}
|
|
wf = self.client.workflows.find(**kwargs)
|
|
|
|
fields = ['wf_id', 'app_id', 'actions', 'config',
|
|
'source', 'id', 'created_at', 'updated_at', 'status']
|
|
self._print_dict(wf, fields, wrap=72)
|
|
|
|
def logs(self):
|
|
"""Show one of an app's live workflows logs."""
|
|
self.parser.add_argument('app',
|
|
help="App uuid or name")
|
|
self.parser.add_argument('workflow',
|
|
help="Workflow id or uuid")
|
|
args = self.parser.parse_args()
|
|
revision = args.workflow
|
|
try:
|
|
revision = int(revision, 10)
|
|
except ValueError:
|
|
revision = args.workflow
|
|
app = self.client.apps.find(name_or_id=args.app)
|
|
display_logs_for_single_workflow(self, app, revision)
|
|
|
|
|
|
class OldAppCommands(cli_utils.CommandsBase):
|
|
"""Commands for working with applications.
|
|
|
|
Available commands:
|
|
solum app list
|
|
Print an index of all deployed applications.
|
|
|
|
solum app show <NAME|UUID>
|
|
Print detailed information about one application.
|
|
|
|
solum app create [--plan-file <PLANFILE>] [--git-url <GIT_URL>]
|
|
[--lp <LANGUAGEPACK>] [--run-cmd <RUN_CMD>]
|
|
[--unittest-cmd <UNITTEST_CMD>]
|
|
[--name <NAME>] [--port <PORT>]
|
|
[--param-file <PARAMFILE>]
|
|
[--desc <DESCRIPTION>]
|
|
[--setup-trigger]
|
|
[--private-repo]
|
|
[--trigger-workflow <WORKFLOW>]
|
|
Register a new application with Solum.
|
|
|
|
solum app deploy <NAME|UUID>
|
|
Deploy an application, building any applicable artifacts first.
|
|
|
|
solum app logs <NAME|UUID>
|
|
Show the logs of an application for all the deployments.
|
|
|
|
solum app delete <NAME|UUID>
|
|
Delete an application and all related artifacts.
|
|
"""
|
|
|
|
def _get_assemblies_by_plan(self, plan):
|
|
# TODO(datsun180b): Write this.
|
|
return []
|
|
|
|
def _validate_plan_file(self, plan_definition):
|
|
if 'artifacts' not in plan_definition:
|
|
raise exc.CommandException(message="Missing artifacts section")
|
|
elif plan_definition['artifacts'] is None:
|
|
raise exc.CommandException(message="Artifacts cannot be empty")
|
|
elif 'content' not in plan_definition['artifacts'][0]:
|
|
raise exc.CommandException(message="Artifact content missing")
|
|
|
|
error_message = ("Application name must be 1-100 characters long, "
|
|
"only contain a-z,0-9,-,_ and start with an alphabet "
|
|
"character.")
|
|
|
|
if plan_definition.get('name') is not None:
|
|
if not name_is_valid(plan_definition.get('name')):
|
|
raise exc.CommandError(message=error_message)
|
|
|
|
def list(self):
|
|
"""Print a list of all deployed applications."""
|
|
# This is just "plan list".
|
|
# TODO(datsun180b): List each plan and its associated
|
|
# assemblies.
|
|
fields = ['uuid', 'name', 'description']
|
|
plans = self.client.plans.list()
|
|
self._print_list(plans, fields)
|
|
|
|
def show(self):
|
|
"""Print detailed information about one application."""
|
|
# This is just "plan show <PLAN>".
|
|
# 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_args()
|
|
try:
|
|
plan = self.client.plans.find(name_or_id=args.app)
|
|
except exceptions.NotFound:
|
|
message = "No app named '%s'." % args.app
|
|
raise exceptions.NotFound(message=message)
|
|
|
|
# Fetch the most recent app_uri.
|
|
assemblies = self.client.assemblies.list()
|
|
app_uri = ''
|
|
updated = ''
|
|
app_status = 'REGISTERED'
|
|
for a in assemblies:
|
|
plan_uuid = a.plan_uri.split('/')[-1]
|
|
if plan_uuid != plan.uuid:
|
|
continue
|
|
if a.updated_at >= updated:
|
|
updated = a.updated_at
|
|
app_uri = a.application_uri
|
|
app_status = a.status
|
|
|
|
plan.application_uri = app_uri
|
|
plan.status = app_status
|
|
fields = ['uuid', 'name', 'description', 'uri', 'artifacts',
|
|
'trigger_uri', 'application_uri', 'status']
|
|
self._print_dict(plan, fields, wrap=72)
|
|
artifacts = copy.deepcopy(vars(plan).get('artifacts'))
|
|
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('--plan-file',
|
|
dest='planfile',
|
|
help="Local planfile location")
|
|
self.parser.add_argument('--git-url',
|
|
help='Source repo')
|
|
self.parser.add_argument('--languagepack',
|
|
help='Language pack')
|
|
self.parser.add_argument('--lp',
|
|
dest='languagepack',
|
|
help='Language pack')
|
|
self.parser.add_argument('--run-cmd',
|
|
help="Application entry point")
|
|
self.parser.add_argument('--unittest-cmd',
|
|
help="Command to execute unit tests")
|
|
self.parser.add_argument('--port',
|
|
type=ValidPort,
|
|
help="The port your application listens on")
|
|
self.parser.add_argument('--name',
|
|
type=ValidName,
|
|
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")
|
|
self.parser.add_argument('--setup-trigger',
|
|
action='store_true',
|
|
dest='setup_trigger',
|
|
help="Set up app trigger on git repo")
|
|
self.parser.add_argument('--private-repo',
|
|
action='store_true',
|
|
dest='private_repo',
|
|
help="Source repo requires authentication.")
|
|
|
|
trigger_help = ("Which of stages build, unittest, deploy to trigger "
|
|
"from git. For example: "
|
|
"--trigger-workflow=unittest+build+deploy. "
|
|
"Implies --setup-trigger.")
|
|
self.parser.add_argument('--trigger-workflow',
|
|
default='',
|
|
dest='workflow',
|
|
help=trigger_help)
|
|
|
|
args = self.parser.parse_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)
|
|
self._validate_plan_file(plan_definition)
|
|
if (plan_definition['artifacts'][0].get('language_pack')
|
|
is not None):
|
|
lp = plan_definition['artifacts'][0]['language_pack']
|
|
if lp != 'auto':
|
|
try:
|
|
lp1 = (
|
|
self.client.languagepacks.find
|
|
(name_or_id=lp)
|
|
)
|
|
except Exception as e:
|
|
if type(e).__name__ == 'NotFound':
|
|
raise exc.CommandError("Languagepack %s "
|
|
"not registered"
|
|
% lp)
|
|
filtered_list = cli_utils.filter_ready_lps([lp1])
|
|
if len(filtered_list) <= 0:
|
|
raise exc.CommandError("Languagepack %s "
|
|
"not READY" % lp)
|
|
if plan_definition['artifacts'][0].get('ports') is None:
|
|
print("No application port specified in plan file.")
|
|
print("Defaulting to port 80.")
|
|
plan_definition['artifacts'][0]['ports'] = 80
|
|
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',
|
|
'name': '',
|
|
'content': {},
|
|
}]}
|
|
|
|
# NOTE: This assumes the plan contains exactly one artifact.
|
|
|
|
# Check the planfile-supplied name first.
|
|
error_message = ("Application name must be 1-100 characters long, "
|
|
"only contain a-z,0-9,-,_ and start with an alphabet "
|
|
"character.")
|
|
app_name = ''
|
|
if plan_definition.get('name') is not None:
|
|
if not name_is_valid(plan_definition.get('name')):
|
|
raise exc.CommandError(message=error_message)
|
|
app_name = plan_definition.get('name')
|
|
# Check the arguments next.
|
|
elif args.name:
|
|
if name_is_valid(args.name):
|
|
app_name = args.name
|
|
# Just ask.
|
|
else:
|
|
while True:
|
|
app_name = input("Please name the application.\n> ")
|
|
if name_is_valid(app_name):
|
|
break
|
|
print(error_message)
|
|
|
|
plan_definition['name'] = app_name
|
|
|
|
# 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.
|
|
|
|
languagepack = None
|
|
if args.languagepack is not None:
|
|
languagepack = args.languagepack
|
|
plan_definition['artifacts'][0]['language_pack'] = languagepack
|
|
elif plan_definition['artifacts'][0].get('language_pack') is None:
|
|
languagepacks = self.client.languagepacks.list()
|
|
filtered_list = cli_utils.filter_ready_lps(languagepacks)
|
|
|
|
if len(filtered_list) > 0:
|
|
lpnames = [lang_pack.name for lang_pack in filtered_list]
|
|
lp_uuids = [lang_pack.uuid for lang_pack in filtered_list]
|
|
fields = ['uuid', 'name', 'description',
|
|
'status', 'source_uri']
|
|
self._print_list(filtered_list, fields)
|
|
languagepack = input("Please choose a languagepack "
|
|
"from the above list.\n> ")
|
|
while languagepack not in lpnames + lp_uuids:
|
|
languagepack = input("You must choose one of "
|
|
"the named language "
|
|
"packs.\n> ")
|
|
plan_definition['artifacts'][0]['language_pack'] = languagepack
|
|
else:
|
|
raise exc.CommandError("No languagepack in READY state. "
|
|
"Create a languagepack first.")
|
|
|
|
# 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 = input("Please specify a git repository URL "
|
|
"for your application.\n> ")
|
|
plan_definition['artifacts'][0]['content']['href'] = git_url
|
|
git_url = plan_definition['artifacts'][0]['content']['href']
|
|
|
|
# If a token is supplied, we won't need to generate one.
|
|
artifact = plan_definition['artifacts'][0]
|
|
repo_token = artifact.get('repo_token')
|
|
|
|
is_private = (args.private_repo or
|
|
artifact['content'].get('private'))
|
|
plan_definition['artifacts'][0]['content']['private'] = is_private
|
|
|
|
git_url = transform_git_url(git_url, is_private)
|
|
plan_definition['artifacts'][0]['content']['href'] = git_url
|
|
|
|
# If we'll be adding a trigger, or the repo is private,
|
|
# we'll need to use a personal access token.
|
|
# The GitHubAuth object will create one if repo_token is null.
|
|
if is_private or args.setup_trigger or args.workflow:
|
|
gha = github.GitHubAuth(git_url, repo_token=repo_token)
|
|
repo_token = repo_token or gha.repo_token
|
|
# Created or provided, the repo token needs to be in the
|
|
# plan data before we call client.plans.create.
|
|
plan_definition['artifacts'][0]['repo_token'] = repo_token
|
|
|
|
# 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 = input("Please specify start/run command for "
|
|
"your application.\n> ")
|
|
plan_definition['artifacts'][0]['run_cmd'] = run_cmd
|
|
|
|
# Check for unit test command
|
|
if args.unittest_cmd is not None:
|
|
plan_definition['artifacts'][0]['unittest_cmd'] = args.unittest_cmd
|
|
|
|
# Check for the port.
|
|
if args.port is not None:
|
|
plan_definition['artifacts'][0]['ports'] = int(args.port)
|
|
if plan_definition['artifacts'][0].get('ports') is None:
|
|
plan_definition['artifacts'][0]['ports'] = 80
|
|
|
|
# Update name and description if specified.
|
|
a_name = ''
|
|
if (plan_definition['artifacts'][0].get('name') is not None
|
|
and plan_definition['artifacts'][0]['name'] != ''):
|
|
a_name = plan_definition['artifacts'][0]['name']
|
|
a_name = a_name.lower()
|
|
else:
|
|
a_name = app_name.lower()
|
|
if not lpname_is_valid(a_name):
|
|
# https://github.com/docker/compose/issues/941
|
|
# Docker build only allows lowercase names for now.
|
|
msg = name_error_message("Artifact names")
|
|
raise exc.CommandError(msg)
|
|
plan_definition['artifacts'][0]['name'] = a_name
|
|
|
|
if args.desc is not None:
|
|
plan_definition['description'] = args.desc
|
|
elif plan_definition.get('description') is None:
|
|
plan_definition['description'] = ''
|
|
|
|
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))
|
|
plan.status = 'REGISTERED'
|
|
fields = ['uuid', 'name', 'description', 'uri', 'artifacts',
|
|
'trigger_uri', 'status']
|
|
|
|
artifacts = copy.deepcopy(vars(plan).get('artifacts'))
|
|
self._print_dict(plan, fields, wrap=72)
|
|
|
|
# Solum generated a keypair; only upload the public key if we already
|
|
# have a repo_token, since we'd only have one if we've authed against
|
|
# github already.
|
|
content = vars(artifacts[0]).get('content')
|
|
if content:
|
|
public_key = content.get('public_key', '')
|
|
if repo_token and is_private and public_key:
|
|
try:
|
|
gha = github.GitHubAuth(git_url, repo_token=repo_token)
|
|
gha.add_ssh_key(public_key=public_key)
|
|
except github.GitHubException as ghe:
|
|
raise exc.CommandError(message=str(ghe))
|
|
|
|
if args.setup_trigger or args.workflow:
|
|
trigger_uri = vars(plan).get('trigger_uri', '')
|
|
if trigger_uri:
|
|
workflow = None
|
|
if args.workflow:
|
|
workflow = args.workflow.replace('+', ' ').split(' ')
|
|
try:
|
|
gha = github.GitHubAuth(git_url, repo_token=repo_token)
|
|
gha.create_webhook(trigger_uri, workflow=workflow)
|
|
except github.GitHubException as ghe:
|
|
raise exc.CommandError(message=str(ghe))
|
|
|
|
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_args()
|
|
try:
|
|
plan = self.client.plans.find(name_or_id=args.app)
|
|
except exceptions.NotFound:
|
|
message = "No app named '%s'." % args.app
|
|
raise exceptions.NotFound(message=message)
|
|
|
|
assembly = self.client.assemblies.create(name=plan.name,
|
|
description=plan.description,
|
|
plan_uri=plan.uri)
|
|
fields = ['uuid', 'name', 'status', 'application_uri']
|
|
self._print_dict(assembly, fields, 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_args()
|
|
try:
|
|
plan = self.client.plans.find(name_or_id=args.app)
|
|
except exceptions.NotFound:
|
|
message = "No app named '%s'." % args.app
|
|
raise exceptions.NotFound(message=message)
|
|
self.client.plans.delete(plan_id=str(plan.uuid))
|
|
|
|
|
|
class InfoCommands(cli_utils.NoSubCommands):
|
|
"""Show Solum server connection information.
|
|
|
|
Available commands:
|
|
|
|
solum info
|
|
Show Solum endpoint and API release version.
|
|
|
|
"""
|
|
|
|
def info(self):
|
|
parsed, _ = self.parser.parse_known_args()
|
|
|
|
# Use the api_endpoint to get the catalog
|
|
|
|
ks_kwargs = {
|
|
'username': parsed.os_username,
|
|
'password': parsed.os_password,
|
|
'tenant_name': parsed.os_tenant_name,
|
|
'auth_url': parsed.os_auth_url,
|
|
}
|
|
|
|
solum_api_endpoint = parsed.solum_url
|
|
if not solum_api_endpoint:
|
|
ksclient = keystoneclient.Client(**ks_kwargs)
|
|
services = ksclient.auth_ref.service_catalog
|
|
services = services.catalog['serviceCatalog']
|
|
solum_service = [s for s in services if s['name'] == 'solum']
|
|
try:
|
|
endpoint = solum_service[0]['endpoints']
|
|
solum_api_endpoint = endpoint[0]['publicURL']
|
|
except (IndexError, KeyError):
|
|
print("Error: SOLUM_URL not set, and no Solum endpoint "
|
|
"could be found in service catalog.")
|
|
|
|
solum_api_version = ''
|
|
if solum_api_endpoint:
|
|
kwargs = {"disable_ssl_certificate_validation": not self.verify}
|
|
try:
|
|
resp, content = httplib2.Http(**kwargs).request(
|
|
solum_api_endpoint, 'GET')
|
|
solum_api_version = resp.get('x-solum-release', '')
|
|
except Exception:
|
|
print("Error: Solum endpoint could not be contacted;"
|
|
" API version could not be determined.")
|
|
|
|
print("python-solumclient version %s" % solumclient.__version__)
|
|
print("solum API endpoint: %s" % solum_api_endpoint)
|
|
print("solum API version: %s" % solum_api_version)
|
|
print("solum auth endpoint: %s" % parsed.os_auth_url)
|
|
print("solum auth username: %s" % parsed.os_username)
|
|
print("solum auth tenant/project: %s" % parsed.os_tenant_name)
|
|
|
|
|
|
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_args(self, *args, **kwargs):
|
|
ns, rem = self.parse_known_args(*args, **kwargs)
|
|
if rem:
|
|
unrec = ', '.join([a.split('=')[0].lstrip('-') for a in rem])
|
|
raise exc.CommandError("Unrecognized arguments: %s" % unrec)
|
|
return ns
|
|
|
|
def parse_known_args(self, *args, **kwargs):
|
|
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.
|
|
|
|
Available commands:
|
|
|
|
solum help
|
|
Show this help message.
|
|
|
|
solum info
|
|
Show Solum endpoint and API release version.
|
|
|
|
solum --version
|
|
Show current Solum client version and exit.
|
|
|
|
solum lp help
|
|
Show a help message specific to languagepack commands.
|
|
|
|
solum lp create <NAME> <GIT_REPO_URL>
|
|
Create a new language pack from a git repo.
|
|
|
|
solum lp list
|
|
Print and index of all available language packs.
|
|
|
|
solum lp show <NAME|UUID>
|
|
Print the details of a language pack.
|
|
|
|
solum lp delete <NAME|UUID>
|
|
Destroy a language pack.
|
|
|
|
solum lp logs <NAME|UUID>
|
|
Show logs for a language pack.
|
|
|
|
solum app help
|
|
Show a help message specific to app commands.
|
|
|
|
solum app list
|
|
Print an index of all deployed applications.
|
|
|
|
solum app show <NAME|UUID>
|
|
Print detailed information about one application.
|
|
|
|
solum app create [--app-file <AppFile>] [--git-url <GIT_URL>]
|
|
[--lp <LANGUAGEPACK>]
|
|
[--param-file <PARAMFILE>]
|
|
[--setup-trigger]
|
|
[--trigger-workflow <CUSTOM-WORKFLOW>]
|
|
<CUSTOM-WORKFLOW>=(unittest | build | unittest+build)
|
|
Without the --trigger-workflow flag,
|
|
the workflow unittest+build+deploy is triggered
|
|
(this is the default workflow)
|
|
|
|
Register a new application with Solum.
|
|
|
|
solum app deploy <NAME|UUID>
|
|
Deploy an application, building any applicable artifacts first.
|
|
du-id is optional flag. It can be used to pass in id of a previously
|
|
created deployment unit. If passed, this command will deploy the du
|
|
referenced by the provided du-id instead of building one first.
|
|
|
|
solum app delete <NAME|UUID>
|
|
Delete an application and all related artifacts.
|
|
|
|
solum app logs <NAME|UUID> [--wf-id <wf-id>]
|
|
Show the logs of an application for all the workflows.
|
|
wf-id is optional flag which can be used to pass in id of one of
|
|
the existing workflows. If provided, the logs only for that workflow
|
|
are displayed.
|
|
|
|
solum app scale <APP_NAME|UUID> <target>
|
|
|
|
solum workflow list <APP_NAME|UUID>
|
|
List all application workflows.
|
|
|
|
solum workflow show <APP_NAME|UUID> <WORKFLOW_ID|UUID>
|
|
Print the details of a workflow.
|
|
|
|
solum workflow logs <APP_NAME|UUID> <WORKFLOW_ID|UUID>
|
|
List all the logs of a given workflow.
|
|
|
|
|
|
SOON TO BE DEPRECATED:
|
|
|
|
solum oldapp create [--plan-file <PLANFILE>] [--git-url <GIT_URL>]
|
|
[--lp <LANGUAGEPACK>] [--run-cmd <RUN_CMD>]
|
|
[--unittest-cmd <UNITTEST_CMD>]
|
|
[--name <NAME>] [--port <PORT>]
|
|
[--param-file <PARAMFILE>]
|
|
[--desc <DESCRIPTION>]
|
|
[--setup-trigger]
|
|
[--private-repo]
|
|
[--trigger-workflow <WORKFLOW>]
|
|
Register a new application with Solum.
|
|
|
|
"""
|
|
|
|
parser = PermissiveParser()
|
|
|
|
resources = {
|
|
'oldapp': OldAppCommands,
|
|
'app': AppCommands,
|
|
'plan': PlanCommands,
|
|
'pipeline': PipelineCommands,
|
|
'lp': LanguagePackCommands,
|
|
'languagepack': LanguagePackCommands,
|
|
'component': ComponentCommands,
|
|
'info': InfoCommands,
|
|
'wf': WorkflowCommands,
|
|
'workflow': WorkflowCommands,
|
|
}
|
|
|
|
choices = resources.keys()
|
|
|
|
parser.add_argument('resource', choices=choices,
|
|
default='help',
|
|
help="Target noun to act upon")
|
|
|
|
parser.add_argument('-V', '--version', action='store_true',
|
|
dest='show_version',
|
|
help="Report solum version.")
|
|
|
|
parser.add_argument('-E', '--show-errors', dest='show_errors',
|
|
action='store_true',
|
|
help='Debug. Show traceback on error.')
|
|
|
|
parsed, _ = parser.parse_known_args()
|
|
|
|
if parsed.show_version:
|
|
print("python-solumclient version %s" % solumclient.__version__)
|
|
return
|
|
|
|
resource = vars(parsed).get('resource')
|
|
|
|
if resource in resources:
|
|
if parsed.show_errors:
|
|
resources[resource](parser)
|
|
else:
|
|
try:
|
|
resources[resource](parser)
|
|
except Exception as e:
|
|
if hasattr(e, 'message'):
|
|
print("ERROR: %s" % e.message)
|
|
else:
|
|
print("ERROR: %s" % e)
|
|
|
|
else:
|
|
print(main.__doc__)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|