518 lines
18 KiB
Python
518 lines
18 KiB
Python
# 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.
|
|
|
|
"""Application-catalog v1 package action implementation"""
|
|
|
|
import collections
|
|
import itertools
|
|
import os
|
|
import shutil
|
|
import six
|
|
import sys
|
|
import tempfile
|
|
import zipfile
|
|
|
|
from osc_lib.command import command
|
|
from osc_lib import exceptions as exc
|
|
from osc_lib import utils
|
|
from oslo_log import log as logging
|
|
|
|
from muranoclient.apiclient import exceptions
|
|
from muranoclient.common import exceptions as common_exceptions
|
|
from muranoclient.common import utils as murano_utils
|
|
from muranoclient.v1.package_creator import hot_package
|
|
from muranoclient.v1.package_creator import mpl_package
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
DEFAULT_REPO_URL = "http://apps.openstack.org/api/v1/murano_repo/liberty/"
|
|
|
|
|
|
class CreatePackage(command.Command):
|
|
"""Create an application package."""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(CreatePackage, self).get_parser(prog_name)
|
|
parser.add_argument(
|
|
'-t', '--template',
|
|
metavar='<HEAT_TEMPLATE>',
|
|
help=("Path to the Heat template to import as "
|
|
"an Application Definition."),
|
|
)
|
|
parser.add_argument(
|
|
'-c', '--classes-dir',
|
|
metavar='<CLASSES_DIRECTORY>',
|
|
help=("Path to the directory containing application classes."),
|
|
)
|
|
parser.add_argument(
|
|
'-r', '--resources-dir',
|
|
metavar='<RESOURCES_DIRECTORY>',
|
|
help=("Path to the directory containing application resources."),
|
|
)
|
|
parser.add_argument(
|
|
'-n', '--name',
|
|
metavar='<DISPLAY_NAME>',
|
|
help=("Display name of the Application in Catalog."),
|
|
)
|
|
parser.add_argument(
|
|
'-f', '--full-name',
|
|
metavar='<full-name>',
|
|
help=("Fully-qualified name of the Application in Catalog."),
|
|
)
|
|
parser.add_argument(
|
|
'-a', '--author',
|
|
metavar='<AUTHOR>',
|
|
help=("Name of the publisher."),
|
|
)
|
|
parser.add_argument(
|
|
'--tags',
|
|
metavar='<TAG1 TAG2>',
|
|
nargs='*',
|
|
help=("A list of keywords connected to the application."),
|
|
)
|
|
parser.add_argument(
|
|
'-d', '--description',
|
|
metavar='<DESCRIPTION>',
|
|
help=("Detailed description for the Application in Catalog."),
|
|
)
|
|
parser.add_argument(
|
|
'-o', '--output',
|
|
metavar='<PACKAGE_NAME>',
|
|
help=("The name of the output file archive to save locally."),
|
|
)
|
|
parser.add_argument(
|
|
'-u', '--ui',
|
|
metavar='<UI_DEFINITION>',
|
|
help=("Dynamic UI form definition."),
|
|
)
|
|
parser.add_argument(
|
|
'--type',
|
|
metavar='<TYPE>',
|
|
help=("Package type. Possible values: Application or Library."),
|
|
)
|
|
parser.add_argument(
|
|
'-l', '--logo',
|
|
metavar='<LOGO>',
|
|
help=("Path to the package logo."),
|
|
)
|
|
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
LOG.debug("take_action({0})".format(parsed_args))
|
|
parsed_args.os_username = os.getenv('OS_USERNAME')
|
|
|
|
def _make_archive(archive_name, path):
|
|
zip_file = zipfile.ZipFile(archive_name, 'w')
|
|
for root, dirs, files in os.walk(path):
|
|
for f in files:
|
|
zip_file.write(os.path.join(root, f),
|
|
arcname=os.path.join(
|
|
os.path.relpath(root, path), f))
|
|
|
|
if parsed_args.template and parsed_args.classes_dir:
|
|
raise exc.CommandError(
|
|
"Provide --template for a HOT-based package, OR"
|
|
" --classes-dir for a MuranoPL-based package")
|
|
if not parsed_args.template and not parsed_args.classes_dir:
|
|
raise exc.CommandError(
|
|
"Provide --template for a HOT-based package, OR at least"
|
|
" --classes-dir for a MuranoPL-based package")
|
|
directory_path = None
|
|
try:
|
|
archive_name = parsed_args.output if parsed_args.output else None
|
|
if parsed_args.template:
|
|
directory_path = hot_package.prepare_package(parsed_args)
|
|
if not archive_name:
|
|
archive_name = os.path.basename(parsed_args.template)
|
|
archive_name = os.path.splitext(archive_name)[0] + ".zip"
|
|
else:
|
|
directory_path = mpl_package.prepare_package(parsed_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)
|
|
|
|
|
|
class ListPackages(command.Lister):
|
|
"""List available packages."""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(ListPackages, self).get_parser(prog_name)
|
|
parser.add_argument(
|
|
"--limit",
|
|
type=int,
|
|
default=0,
|
|
help='Show limited number of packages'
|
|
)
|
|
parser.add_argument(
|
|
"--marker",
|
|
default='',
|
|
help='Show packages starting from package with id excluding it'
|
|
)
|
|
parser.add_argument(
|
|
"--include-disabled",
|
|
default=False,
|
|
action="store_true"
|
|
)
|
|
parser.add_argument(
|
|
"--owned",
|
|
default=False,
|
|
action="store_true"
|
|
)
|
|
parser.add_argument(
|
|
'--search',
|
|
metavar='<SEARCH_KEYS>',
|
|
dest='search',
|
|
required=False,
|
|
help='Show packages, that match search keys fuzzily'
|
|
)
|
|
parser.add_argument(
|
|
'--name',
|
|
metavar='<PACKAGE_NAME>',
|
|
dest='name',
|
|
required=False,
|
|
help='Show packages, whose name match parameter exactly'
|
|
)
|
|
parser.add_argument(
|
|
'--fqn',
|
|
metavar="<PACKAGE_FULLY_QUALIFIED_NAME>",
|
|
dest='fqn',
|
|
required=False,
|
|
help='Show packages, '
|
|
'whose fully qualified name match parameter exactly'
|
|
)
|
|
parser.add_argument(
|
|
'--type',
|
|
metavar='<PACKAGE_TYPE>',
|
|
dest='type',
|
|
required=False,
|
|
help='Show packages, whose type match parameter exactly'
|
|
)
|
|
parser.add_argument(
|
|
'--category',
|
|
metavar='<PACKAGE_CATEGORY>',
|
|
dest='category',
|
|
required=False,
|
|
help='Show packages, whose categories include parameter'
|
|
)
|
|
parser.add_argument(
|
|
'--class_name',
|
|
metavar='<PACKAGE_CLASS_NAME>',
|
|
dest='class_name',
|
|
required=False,
|
|
help='Show packages, whose class name match parameter exactly'
|
|
)
|
|
parser.add_argument(
|
|
'--tag',
|
|
metavar='<PACKAGE_TAG>',
|
|
dest='tag',
|
|
required=False,
|
|
help='Show packages, whose tags include parameter'
|
|
)
|
|
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
LOG.debug("take_action({0})".format(parsed_args))
|
|
client = self.app.client_manager.application_catalog
|
|
filter_args = {
|
|
"include_disabled": getattr(parsed_args,
|
|
'include_disabled', False),
|
|
"owned": getattr(parsed_args, 'owned', False),
|
|
}
|
|
if parsed_args:
|
|
if parsed_args.limit < 0:
|
|
raise exceptions.CommandError(
|
|
'--limit parameter must be non-negative')
|
|
if parsed_args.limit != 0:
|
|
filter_args['limit'] = parsed_args.limit
|
|
if parsed_args.marker:
|
|
filter_args['marker'] = parsed_args.marker
|
|
if parsed_args.search:
|
|
filter_args['search'] = parsed_args.search
|
|
if parsed_args.name:
|
|
filter_args['name'] = parsed_args.name
|
|
if parsed_args.fqn:
|
|
filter_args['fqn'] = parsed_args.fqn
|
|
if parsed_args.type:
|
|
filter_args['type'] = parsed_args.type
|
|
if parsed_args.category:
|
|
filter_args['category'] = parsed_args.category
|
|
if parsed_args.class_name:
|
|
filter_args['class_name'] = parsed_args.class_name
|
|
if parsed_args.tag:
|
|
filter_args['tag'] = parsed_args.tag
|
|
|
|
data = client.packages.filter(**filter_args)
|
|
|
|
columns = ('id', 'name', 'fully_qualified_name', 'author', 'active',
|
|
'is public', 'type', 'version')
|
|
column_headers = [c.capitalize() for c in columns]
|
|
if not parsed_args or parsed_args.limit == 0:
|
|
return (
|
|
column_headers,
|
|
list(utils.get_item_properties(
|
|
s,
|
|
columns,
|
|
) for s in data)
|
|
)
|
|
else:
|
|
return (
|
|
column_headers,
|
|
list(utils.get_item_properties(
|
|
s,
|
|
columns,
|
|
) for s in itertools.islice(data, parsed_args.limit))
|
|
)
|
|
|
|
|
|
class DeletePackage(command.Lister):
|
|
"""Delete a package."""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(DeletePackage, self).get_parser(prog_name)
|
|
parser.add_argument(
|
|
'id',
|
|
metavar="<ID>",
|
|
nargs="+",
|
|
help="Package ID to delete.",
|
|
)
|
|
|
|
return parser
|
|
|
|
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 package_id in parsed_args.id:
|
|
try:
|
|
client.packages.delete(package_id)
|
|
except exceptions.NotFound:
|
|
failure_count += 1
|
|
print("Failed to delete '{0}'; package not found".
|
|
format(package_id))
|
|
|
|
if failure_count == len(parsed_args.id):
|
|
raise exceptions.CommandError("Unable to find and delete any of "
|
|
"the specified packages.")
|
|
data = client.packages.filter()
|
|
|
|
columns = ('id', 'name', 'fully_qualified_name', 'author', 'active',
|
|
'is public', 'type', 'version')
|
|
column_headers = [c.capitalize() for c in columns]
|
|
|
|
return (
|
|
column_headers,
|
|
list(utils.get_item_properties(
|
|
s,
|
|
columns,
|
|
) for s in data)
|
|
)
|
|
|
|
|
|
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
|
|
|
|
|
|
class ImportPackage(command.Lister):
|
|
"""Import a package."""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(ImportPackage, self).get_parser(prog_name)
|
|
parser.add_argument(
|
|
'filename',
|
|
metavar='<FILE>',
|
|
nargs='+',
|
|
help='URL of the murano zip package, FQPN, path to zip package'
|
|
' or path to directory with package.'
|
|
)
|
|
parser.add_argument(
|
|
'--categories',
|
|
metavar='<CATEGORY>',
|
|
nargs='*',
|
|
help='Category list to attach.',
|
|
)
|
|
parser.add_argument(
|
|
'--is-public',
|
|
action='store_true',
|
|
default=False,
|
|
help="Make the package available for users from other tenants.",
|
|
)
|
|
parser.add_argument(
|
|
'--package-version',
|
|
default='',
|
|
help='Version of the package to use from repository '
|
|
'(ignored when importing with multiple packages).'
|
|
)
|
|
parser.add_argument(
|
|
'--exists-action',
|
|
default='',
|
|
choices=['a', 's', 'u'],
|
|
help='Default action when a package already exists: '
|
|
'(s)kip, (u)pdate, (a)bort.'
|
|
)
|
|
parser.add_argument(
|
|
'--dep-exists-action',
|
|
default='',
|
|
choices=['a', 's', 'u'],
|
|
help='Default action when a dependency package already exists: '
|
|
'(s)kip, (u)pdate, (a)bort.'
|
|
)
|
|
parser.add_argument('--murano-repo-url',
|
|
default=murano_utils.env(
|
|
'MURANO_REPO_URL',
|
|
default=DEFAULT_REPO_URL),
|
|
help=('Defaults to env[MURANO_REPO_URL] '
|
|
'or {0}'.format(DEFAULT_REPO_URL)))
|
|
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
LOG.debug("take_action({0})".format(parsed_args))
|
|
|
|
client = self.app.client_manager.application_catalog
|
|
|
|
data = {"is_public": parsed_args.is_public}
|
|
version = parsed_args.package_version
|
|
if version and len(parsed_args.filename) >= 2:
|
|
print("Requested to import more than one package, "
|
|
"ignoring version.")
|
|
version = ''
|
|
|
|
if parsed_args.categories:
|
|
data["categories"] = parsed_args.categories
|
|
|
|
total_reqs = collections.OrderedDict()
|
|
main_packages_names = []
|
|
for filename in parsed_args.filename:
|
|
if os.path.isfile(filename) or os.path.isdir(filename):
|
|
_file = filename
|
|
else:
|
|
print("Package file '{0}' does not exist, attempting to "
|
|
"download".format(filename))
|
|
_file = murano_utils.to_url(
|
|
filename,
|
|
version=version,
|
|
base_url=parsed_args.murano_repo_url,
|
|
extension='.zip',
|
|
path='apps/',
|
|
)
|
|
try:
|
|
package = murano_utils.Package.from_file(_file)
|
|
except Exception as e:
|
|
print("Failed to create package for '{0}', reason: {1}".format(
|
|
filename, e))
|
|
continue
|
|
total_reqs.update(
|
|
package.requirements(base_url=parsed_args.murano_repo_url))
|
|
main_packages_names.append(package.manifest['FullName'])
|
|
|
|
imported_list = []
|
|
|
|
dep_exists_action = parsed_args.dep_exists_action
|
|
if dep_exists_action == '':
|
|
dep_exists_action = parsed_args.exists_action
|
|
|
|
for name, package in six.iteritems(total_reqs):
|
|
image_specs = package.images()
|
|
if image_specs:
|
|
print("Inspecting required images")
|
|
try:
|
|
imgs = murano_utils.ensure_images(
|
|
glance_client=client.glance_client,
|
|
image_specs=image_specs,
|
|
base_url=parsed_args.murano_repo_url,
|
|
is_package_public=parsed_args.is_public)
|
|
for img in imgs:
|
|
print("Added {0}, {1} image".format(
|
|
img['name'], img['id']))
|
|
except Exception as e:
|
|
print("Error {0} occurred while installing "
|
|
"images for {1}".format(e, name))
|
|
|
|
if name in main_packages_names:
|
|
exists_action = parsed_args.exists_action
|
|
else:
|
|
exists_action = dep_exists_action
|
|
try:
|
|
imported_package = _handle_package_exists(
|
|
client, data, package, exists_action)
|
|
if imported_package:
|
|
imported_list.append(imported_package)
|
|
except Exception as e:
|
|
print("Error {0} occurred while installing package {1}".format(
|
|
e, name))
|
|
|
|
columns = ('id', 'name', 'fully_qualified_name', 'author', 'active',
|
|
'is public', 'type', 'version')
|
|
column_headers = [c.capitalize() for c in columns]
|
|
|
|
return (
|
|
column_headers,
|
|
list(utils.get_item_properties(
|
|
s,
|
|
columns,
|
|
) for s in imported_list)
|
|
)
|