diff --git a/muranoclient/osc/v1/package.py b/muranoclient/osc/v1/package.py new file mode 100644 index 00000000..31d3c78b --- /dev/null +++ b/muranoclient/osc/v1/package.py @@ -0,0 +1,138 @@ +# 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 os +import shutil +import tempfile +import zipfile + +from muranoclient.v1.package_creator import hot_package +from muranoclient.v1.package_creator import mpl_package +from osc_lib.command import command +from osc_lib import exceptions as exc +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + + +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='', + help=("Path to the Heat template to import as " + "an Application Definition."), + ) + parser.add_argument( + '-c', '--classes-dir', + metavar='', + help=("Path to the directory containing application classes."), + ) + parser.add_argument( + '-r', '--resources-dir', + metavar='', + help=("Path to the directory containing application resources."), + ) + parser.add_argument( + '-n', '--name', + metavar='', + help=("Display name of the Application in Catalog."), + ) + parser.add_argument( + '-f', '--full-name', + metavar='', + help=("Fully-qualified name of the Application in Catalog."), + ) + parser.add_argument( + '-a', '--author', + metavar='', + help=("Name of the publisher."), + ) + parser.add_argument( + '--tags', + metavar='', + nargs='*', + help=("A list of keywords connected to the application."), + ) + parser.add_argument( + '-d', '--description', + metavar='', + help=("Detailed description for the Application in Catalog."), + ) + parser.add_argument( + '-o', '--output', + metavar='', + help=("The name of the output file archive to save locally."), + ) + parser.add_argument( + '-u', '--ui', + metavar='', + help=("Dynamic UI form definition."), + ) + parser.add_argument( + '--type', + metavar='', + help=("Package type. Possible values: Application or Library."), + ) + parser.add_argument( + '-l', '--logo', + metavar='', + 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) diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/heat-template.yaml b/muranoclient/tests/unit/osc/v1/fixture_data/heat-template.yaml new file mode 100644 index 00000000..d7e80909 --- /dev/null +++ b/muranoclient/tests/unit/osc/v1/fixture_data/heat-template.yaml @@ -0,0 +1 @@ +heat_template_version: 2013-05-23 diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/logo.png b/muranoclient/tests/unit/osc/v1/fixture_data/logo.png new file mode 100644 index 00000000..e1a24717 Binary files /dev/null and b/muranoclient/tests/unit/osc/v1/fixture_data/logo.png differ diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Classes/testapp.yaml b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Classes/testapp.yaml new file mode 100644 index 00000000..95b1210a --- /dev/null +++ b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Classes/testapp.yaml @@ -0,0 +1,35 @@ +Namespaces: + =: io.murano.apps.test + std: io.murano + res: io.murano.resources + +Name: APP + +Extends: std:Application + +Properties: + name: + Contract: $.string().notNull() + + instance: + Contract: $.class(res:Instance).notNull() + + +Workflow: + initialize: + Body: + - $.environment: $.find(std:Environment).require() + + deploy: + Body: + - $securityGroupIngress: + - ToPort: 23 + FromPort: 23 + IpProtocol: tcp + External: True + - $.environment.securityGroupManager.addGroupIngress($securityGroupIngress) + - $.instance.deploy() + - $resources: new('io.murano.system.Resources') + - $template: $resources.yaml('Deploy.template') + - $.instance.agent.call($template, $resources) + diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/Deploy.template b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/Deploy.template new file mode 100644 index 00000000..04bd4d5e --- /dev/null +++ b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/Deploy.template @@ -0,0 +1,21 @@ +FormatVersion: 2.0.0 +Version: 1.0.0 +Name: Deploy + +Parameters: + appName: $appName + +Body: | + return deploy(args.appName).stdout + +Scripts: + deploy: + Type: Application + Version: 1.0.0 + EntryPoint: deploy.sh + Files: + - installer.sh + - common.sh + Options: + captureStdout: true + captureStderr: false diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/common.sh b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/common.sh new file mode 100644 index 00000000..a9bf588e --- /dev/null +++ b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/common.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/deploy.sh b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/deploy.sh new file mode 100644 index 00000000..a9bf588e --- /dev/null +++ b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/deploy.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/installer.sh b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/installer.sh new file mode 100644 index 00000000..a9bf588e --- /dev/null +++ b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/installer.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/ui.yaml b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/ui.yaml new file mode 100644 index 00000000..939ba388 --- /dev/null +++ b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/ui.yaml @@ -0,0 +1 @@ +Version: 2 diff --git a/muranoclient/tests/unit/osc/v1/test_package.py b/muranoclient/tests/unit/osc/v1/test_package.py new file mode 100644 index 00000000..28a4f575 --- /dev/null +++ b/muranoclient/tests/unit/osc/v1/test_package.py @@ -0,0 +1,103 @@ +# 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. + +import os +import six +import sys +import tempfile + +from testtools import matchers + +from muranoclient.osc.v1 import package as osc_pkg +from muranoclient.tests.unit.osc.v1 import fakes + +from osc_lib import exceptions as exc + +FIXTURE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), + 'fixture_data')) + + +class TestPackage(fakes.TestApplicationCatalog): + def setUp(self): + super(TestPackage, self).setUp() + self.package_mock = self.app.client_manager.application_catalog.\ + packages + self.package_mock.reset_mock() + + +class TestCreatePackage(TestPackage): + def setUp(self): + super(TestCreatePackage, self).setUp() + + # Command to test + self.cmd = osc_pkg.CreatePackage(self.app, None) + + def test_create_package_without_args(self): + arglist = [] + parsed_args = self.check_parser(self.cmd, arglist, []) + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, parsed_args) + self.assertEqual('Provide --template for a HOT-based package, OR at ' + 'least --classes-dir for a MuranoPL-based package', + str(error)) + + def test_create_package_template_and_classes_args(self): + heat_template = os.path.join(FIXTURE_DIR, 'heat-template.yaml') + classes_dir = os.path.join(FIXTURE_DIR, 'test-app', 'Classes') + arglist = ['--template', heat_template, '--classes-dir', classes_dir] + parsed_args = self.check_parser(self.cmd, arglist, []) + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, parsed_args) + self.assertEqual('Provide --template for a HOT-based package, OR' + ' --classes-dir for a MuranoPL-based package', + str(error)) + + def test_create_hot_based_package(self): + with tempfile.NamedTemporaryFile() as f: + RESULT_PACKAGE = f.name + heat_template = os.path.join(FIXTURE_DIR, 'heat-template.yaml') + logo = os.path.join(FIXTURE_DIR, 'logo.png') + arglist = ['--template', heat_template, '--output', RESULT_PACKAGE, + '-l', logo] + parsed_args = self.check_parser(self.cmd, arglist, []) + orig = sys.stdout + try: + sys.stdout = six.StringIO() + self.cmd.take_action(parsed_args) + finally: + stdout = sys.stdout.getvalue() + sys.stdout.close() + sys.stdout = orig + matchers.MatchesRegex(stdout, + "Application package " + "is available at {0}".format(RESULT_PACKAGE)) + + def test_create_mpl_package(self): + with tempfile.NamedTemporaryFile() as f: + RESULT_PACKAGE = f.name + classes_dir = os.path.join(FIXTURE_DIR, 'test-app', 'Classes') + resources_dir = os.path.join(FIXTURE_DIR, 'test-app', 'Resources') + ui = os.path.join(FIXTURE_DIR, 'test-app', 'ui.yaml') + arglist = ['-c', classes_dir, '-r', resources_dir, + '-u', ui, '-o', RESULT_PACKAGE] + parsed_args = self.check_parser(self.cmd, arglist, []) + orig = sys.stdout + try: + sys.stdout = six.StringIO() + self.cmd.take_action(parsed_args) + finally: + stdout = sys.stdout.getvalue() + sys.stdout.close() + sys.stdout = orig + matchers.MatchesRegex(stdout, + "Application package " + "is available at {0}".format(RESULT_PACKAGE)) diff --git a/setup.cfg b/setup.cfg index 37b246e5..9d2b21c3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,6 +51,8 @@ openstack.application_catalog.v1 = deployment_list = muranoclient.osc.v1.deployment:ListDeployment + package_create = muranoclient.osc.v1.package:CreatePackage + static-action_call = muranoclient.osc.v1.action:StaticActionCall class-schema = muranoclient.osc.v1.schema:ShowSchema