Add package-create command

This command composes application package from heat
templates and for muranoPL classes. Manifest file
is generated automatically. For a list values, such as
tags and categories comma is used.

Partly-implements: blueprint murano-cli-client

Change-Id: I6005bf24761e5537681156fe4fa17bcc11bc7501
This commit is contained in:
Ekaterina Fedorova 2014-06-10 22:09:31 +04:00
parent 1a7db5f125
commit d62b0f2d6e
21 changed files with 951 additions and 19 deletions

6
.gitignore vendored
View File

@ -24,3 +24,9 @@ ChangeLog
#Autogenerated Documentation #Autogenerated Documentation
doc/source/api doc/source/api
#Testing framework
.testrepository
.coverage
*,cover
cover

View File

@ -0,0 +1,21 @@
# Copyright (c) 2014 Mirantis, Inc.
#
# 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
_ROOT = os.path.abspath(os.path.dirname(__file__))
def get_resource(path):
return os.path.join(_ROOT, 'data', path)

View File

@ -16,12 +16,17 @@
from __future__ import print_function from __future__ import print_function
import os import os
import re
import sys import sys
import textwrap import textwrap
import types
import uuid import uuid
import yaml
import prettytable import prettytable
import six import six
import yaql
import yaql.exceptions
from muranoclient.common import exceptions from muranoclient.common import exceptions
from muranoclient.openstack.common import importutils from muranoclient.openstack.common import importutils
@ -156,3 +161,53 @@ def exception_to_str(exc):
error = ("Caught '%(exception)s' exception." % error = ("Caught '%(exception)s' exception." %
{"exception": exc.__class__.__name__}) {"exception": exc.__class__.__name__})
return strutils.safe_encode(error, errors='ignore') return strutils.safe_encode(error, errors='ignore')
class YaqlExpression(object):
def __init__(self, expression):
self._expression = str(expression)
self._parsed_expression = yaql.parse(self._expression)
def expression(self):
return self._expression
def __repr__(self):
return 'YAQL(%s)' % self._expression
def __str__(self):
return self._expression
@staticmethod
def match(expr):
if not isinstance(expr, types.StringTypes):
return False
if re.match('^[\s\w\d.:]*$', expr):
return False
try:
yaql.parse(expr)
return True
except yaql.exceptions.YaqlGrammarException:
return False
except yaql.exceptions.YaqlLexicalException:
return False
def evaluate(self, data=None, context=None):
return self._parsed_expression.evaluate(data=data, context=context)
class YaqlYamlLoader(yaml.Loader):
pass
# workaround for PyYAML bug: http://pyyaml.org/ticket/221
resolvers = {}
for k, v in yaml.Loader.yaml_implicit_resolvers.items():
resolvers[k] = v[:]
YaqlYamlLoader.yaml_implicit_resolvers = resolvers
def yaql_constructor(loader, node):
value = loader.construct_scalar(node)
return YaqlExpression(value)
yaml.add_constructor(u'!yaql', yaql_constructor, YaqlYamlLoader)
yaml.add_implicit_resolver(u'!yaql', YaqlExpression, Loader=YaqlYamlLoader)

View File

@ -1,4 +1,3 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 Nebula, Inc. # Copyright 2011 Nebula, Inc.
# Copyright 2013 Alessio Ababilov # Copyright 2013 Alessio Ababilov
# Copyright 2013 OpenStack Foundation # Copyright 2013 OpenStack Foundation

View File

@ -1,13 +0,0 @@
# Copyright (c) 2013 Mirantis, Inc.
#
# 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.

View File

@ -0,0 +1,45 @@
# Copyright 2011 OpenStack Foundation
# All Rights Reserved.
#
# 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 fixtures
import six
import testtools
class TestCaseShell(testtools.TestCase):
TEST_REQUEST_BASE = {
'verify': True,
}
def setUp(self):
super(TestCaseShell, self).setUp()
if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
os.environ.get('OS_STDOUT_CAPTURE') == '1'):
stdout = self.useFixture(fixtures.StringStream('stdout')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
os.environ.get('OS_STDERR_CAPTURE') == '1'):
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
class TestAdditionalAsserts(testtools.TestCase):
def check_dict_is_subset(self, dict1, dict2):
# There is an assert for this in Python 2.7 but not 2.6
self.assertTrue(all(k in dict2 and dict2[k] == v for k, v
in six.iteritems(dict1)))

View File

@ -0,0 +1 @@
heat_template_version: 2013-05-23

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1 @@
#!/bin/bash

View File

@ -0,0 +1 @@
#!/bin/bash

View File

@ -0,0 +1 @@
#!/bin/bash

View File

@ -0,0 +1 @@
Version: 2

View File

@ -0,0 +1,198 @@
# Copyright (c) 2014 Mirantis, Inc.
#
# 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 shutil
from muranoclient.openstack.common.apiclient import exceptions
from muranoclient.tests import base
from muranoclient.v1.package_creator import hot_package
from muranoclient.v1.package_creator import mpl_package
FIXTURE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
'fixture_data'))
TEMPLATE = os.path.join(FIXTURE_DIR, 'heat-template.yaml')
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')
LOGO = os.path.join(FIXTURE_DIR, 'logo.png')
class TestArgs(object):
pass
class PackageCreatorTest(base.TestAdditionalAsserts):
def test_generate_hot_manifest(self):
args = TestArgs()
args.template = TEMPLATE
args.name = 'test_name'
args.author = 'TestAuthor'
args.full_name = None
args.tags = None
args.description = None
expected_manifest = {
'Format': 'Heat.HOT/1.0',
'Type': 'Application',
'FullName': 'io.murano.apps.generated.TestName',
'Name': 'test_name',
'Description': 'Heat-defined application '
'for a template "heat-template.yaml"',
'Author': 'TestAuthor',
'Tags': ['Heat-generated']
}
result_manifest = hot_package.generate_manifest(args)
self.check_dict_is_subset(expected_manifest, result_manifest)
def test_generate_hot_manifest_nonexistent_template(self):
args = TestArgs()
args.template = '/home/this/path/does/not/exist'
self.assertRaises(exceptions.CommandError,
hot_package.generate_manifest,
args)
def test_generate_hot_manifest_with_all_parameters(self):
args = TestArgs()
args.template = TEMPLATE
args.name = 'test_name'
args.author = 'TestAuthor'
args.full_name = 'test.full.name.TestName'
args.tags = ['test', 'tag', 'Heat']
args.description = 'Test description'
expected_manifest = {
'Format': 'Heat.HOT/1.0',
'Type': 'Application',
'FullName': 'test.full.name.TestName',
'Name': 'test_name',
'Description': 'Test description',
'Author': 'TestAuthor',
'Tags': ['test', 'tag', 'Heat']
}
result_manifest = hot_package.generate_manifest(args)
self.check_dict_is_subset(expected_manifest, result_manifest)
def test_generate_hot_manifest_template_not_yaml(self):
args = TestArgs()
args.template = LOGO
args.name = None
args.full_name = None
self.assertRaises(exceptions.CommandError,
hot_package.generate_manifest, args)
def test_prepare_hot_package(self):
args = TestArgs()
args.template = TEMPLATE
args.name = 'test_name'
args.author = 'TestAuthor'
args.full_name = 'test.full.name.TestName'
args.tags = 'test, tag, Heat'
args.description = 'Test description'
args.logo = LOGO
package_dir = hot_package.prepare_package(args)
prepared_files = ['manifest.yaml', 'logo.png', 'template.yaml']
self.assertEqual(sorted(os.listdir(package_dir)),
sorted(prepared_files))
shutil.rmtree(package_dir)
def test_generate_mpl_manifest(self):
args = TestArgs()
args.template = TEMPLATE
args.classes_dir = CLASSES_DIR
args.resources_dir = RESOURCES_DIR
args.type = 'Application'
args.author = 'TestAuthor'
args.name = None
args.full_name = None
args.tags = None
args.description = None
expected_manifest = {
'Format': 'MuranoPL/1.0',
'Type': 'Application',
'Classes': {'io.murano.apps.test.APP': 'testapp.yaml'},
'FullName': 'io.murano.apps.test.APP',
'Name': 'APP',
'Description': 'Description for the application is not provided',
'Author': 'TestAuthor',
}
result_manifest = mpl_package.generate_manifest(args)
self.check_dict_is_subset(expected_manifest, result_manifest)
def test_generate_mpl_manifest_with_all_parameters(self):
args = TestArgs()
args.template = TEMPLATE
args.classes_dir = CLASSES_DIR
args.resources_dir = RESOURCES_DIR
args.type = 'Application'
args.name = 'test_name'
args.author = 'TestAuthor'
args.full_name = 'test.full.name.TestName'
args.tags = ['test', 'tag', 'Heat']
args.description = 'Test description'
expected_manifest = {
'Format': 'MuranoPL/1.0',
'Type': 'Application',
'Classes': {'io.murano.apps.test.APP': 'testapp.yaml'},
'FullName': 'test.full.name.TestName',
'Name': 'test_name',
'Description': 'Test description',
'Author': 'TestAuthor',
'Tags': ['test', 'tag', 'Heat']
}
result_manifest = mpl_package.generate_manifest(args)
self.check_dict_is_subset(expected_manifest, result_manifest)
def test_generate_mpl_wrong_classes_dir(self):
args = TestArgs()
args.classes_dir = '/home/this/path/does/not/exist'
expected = ("'--classes-dir' parameter should be a directory", )
try:
mpl_package.generate_manifest(args)
except exceptions.CommandError as message:
self.assertEqual(expected, message.args)
def test_generate_mpl_wrong_resources_dir(self):
args = TestArgs()
args.classes_dir = CLASSES_DIR
args.resources_dir = '/home/this/path/does/not/exist'
expected = ("'--resources-dir' parameter should be a directory", )
try:
mpl_package.generate_manifest(args)
except exceptions.CommandError as message:
self.assertEqual(expected, message.args)
def test_prepare_mpl_package(self):
args = TestArgs()
args.template = TEMPLATE
args.classes_dir = CLASSES_DIR
args.resources_dir = RESOURCES_DIR
args.type = 'Application'
args.name = 'test_name'
args.author = 'TestAuthor'
args.full_name = 'test.full.name.TestName'
args.tags = 'test, tag, Heat'
args.description = 'Test description'
args.ui = UI
args.logo = LOGO
prepared_files = ['UI', 'Classes', 'manifest.yaml',
'Resources', 'logo.png']
package_dir = mpl_package.prepare_package(args)
self.assertEqual(sorted(os.listdir(package_dir)),
sorted(prepared_files))
shutil.rmtree(package_dir)

View File

@ -0,0 +1,184 @@
# Copyright (c) 2013 Mirantis, Inc.
#
# 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 re
import six
import sys
import fixtures
import mock
from testtools import matchers
from muranoclient.openstack.common.apiclient import exceptions
import muranoclient.shell
from muranoclient.tests import base
FIXTURE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
'fixture_data'))
RESULT_PACKAGE = os.path.join(FIXTURE_DIR, 'test-app.zip')
FAKE_ENV = {'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_NAME': 'tenant_name',
'OS_AUTH_URL': 'http://no.where'}
FAKE_ENV2 = {'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_ID': 'tenant_id',
'OS_AUTH_URL': 'http://no.where'}
class ShellTest(base.TestCaseShell):
def make_env(self, exclude=None, fake_env=FAKE_ENV):
env = dict((k, v) for k, v in fake_env.items() if k != exclude)
self.useFixture(fixtures.MonkeyPatch('os.environ', env))
def setUp(self):
super(ShellTest, self).setUp()
self.useFixture(fixtures.MonkeyPatch(
'keystoneclient.v2_0.client.Client', mock.MagicMock))
def shell(self, argstr, exitcodes=(0,)):
orig = sys.stdout
orig_stderr = sys.stderr
try:
sys.stdout = six.StringIO()
sys.stderr = six.StringIO()
_shell = muranoclient.shell.MuranoShell()
_shell.main(argstr.split())
except SystemExit:
exc_type, exc_value, exc_traceback = sys.exc_info()
self.assertIn(exc_value.code, exitcodes)
finally:
stdout = sys.stdout.getvalue()
sys.stdout.close()
sys.stdout = orig
stderr = sys.stderr.getvalue()
sys.stderr.close()
sys.stderr = orig_stderr
return (stdout, stderr)
def test_help_unknown_command(self):
self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo')
def test_help(self):
required = [
'.*?^usage: murano',
'.*?^\s+package-create\s+Create an application package.',
'.*?^See "murano help COMMAND" for help on a specific command',
]
stdout, stderr = self.shell('help')
for r in required:
self.assertThat((stdout + stderr),
matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE))
def test_help_on_subcommand(self):
required = [
'.*?^usage: murano package-create',
'.*?^Create an application package.',
]
stdout, stderr = self.shell('help package-create')
for r in required:
self.assertThat((stdout + stderr),
matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE))
def test_help_no_options(self):
required = [
'.*?^usage: murano',
'.*?^\s+package-create\s+Create an application package',
'.*?^See "murano help COMMAND" for help on a specific command',
]
stdout, stderr = self.shell('')
for r in required:
self.assertThat((stdout + stderr),
matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE))
def test_no_username(self):
required = ('You must provide a username via either --os-username or '
'env[OS_USERNAME] or a token via --os-auth-token or '
'env[OS_AUTH_TOKEN]',)
self.make_env(exclude='OS_USERNAME')
try:
self.shell('package-list')
except exceptions.CommandError as message:
self.assertEqual(required, message.args)
else:
self.fail('CommandError not raised')
def test_no_tenant_name(self):
required = ('You must provide a tenant name '
'or tenant id via --os-tenant-name, '
'--os-tenant-id, env[OS_TENANT_NAME] '
'or env[OS_TENANT_ID]',)
self.make_env(exclude='OS_TENANT_NAME')
try:
self.shell('package-list')
except exceptions.CommandError as message:
self.assertEqual(required, message.args)
else:
self.fail('CommandError not raised')
def test_no_tenant_id(self):
required = ('You must provide a tenant name '
'or tenant id via --os-tenant-name, '
'--os-tenant-id, env[OS_TENANT_NAME] '
'or env[OS_TENANT_ID]',)
self.make_env(exclude='OS_TENANT_ID', fake_env=FAKE_ENV2)
try:
self.shell('package-list')
except exceptions.CommandError as message:
self.assertEqual(required, message.args)
else:
self.fail('CommandError not raised')
def test_no_auth_url(self):
required = ('You must provide an auth url'
' via either --os-auth-url or via env[OS_AUTH_URL]',)
self.make_env(exclude='OS_AUTH_URL')
try:
self.shell('package-list')
except exceptions.CommandError as message:
self.assertEqual(required, message.args)
else:
self.fail('CommandError not raised')
def test_create_hot_based_package(self):
self.useFixture(fixtures.MonkeyPatch(
'muranoclient.v1.client.Client', mock.MagicMock))
heat_template = os.path.join(FIXTURE_DIR, 'heat-template.yaml')
logo = os.path.join(FIXTURE_DIR, 'logo.png')
self.make_env()
stdout, stderr = self.shell(
"package-create --template={0} "
"--output={1} -l={2}".format(heat_template, RESULT_PACKAGE, logo))
matchers.MatchesRegex((stdout + stderr),
"Application package "
"is available at {0}".format(RESULT_PACKAGE))
def test_create_mpl_package(self):
self.useFixture(fixtures.MonkeyPatch(
'muranoclient.v1.client.Client', mock.MagicMock))
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')
self.make_env()
stdout, stderr = self.shell(
"package-create -c={0} -r={1} -u={2} -o={3}".format(
classes_dir, resources_dir, ui, RESULT_PACKAGE))
matchers.MatchesRegex((stdout + stderr),
"Application package "
"is available at {0}".format(RESULT_PACKAGE))

View File

@ -0,0 +1,95 @@
# Copyright (c) 2014 Mirantis, Inc.
#
# 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 shutil
import tempfile
import yaml
import muranoclient
from muranoclient.openstack.common.apiclient import exceptions
def generate_manifest(args):
"""Generates application manifest file.
If some parameters are missed - they we be generated automatically.
:param args:
:returns: dictionary, contains manifest file data
"""
if not os.path.isfile(args.template):
raise exceptions.CommandError(
"Template '{0}' doesn`t exist".format(args.template))
filename = os.path.basename(args.template)
if not args.name:
args.name = os.path.splitext(filename)[0]
if not args.full_name:
prefix = 'io.murano.apps.generated'
# normalized_name = re.sub(r'\W+', '_', args.name).title()
normalized_name = args.name.replace('_', ' ').title().replace(' ', '')
args.full_name = '{0}.{1}'.format(prefix, normalized_name)
try:
with open(args.template) as heat_file:
yaml_content = yaml.load(heat_file)
if not args.description:
args.description = yaml_content.get(
'description',
'Heat-defined application for a template "{0}"'.format(
filename))
except yaml.YAMLError:
raise exceptions.CommandError(
"Heat template, represented by --'template' parameter"
" should be a valid yaml file")
if not args.author:
args.author = args.os_username
if not args.tags:
args.tags = ['Heat-generated']
manifest = {
'Format': 'Heat.HOT/1.0',
'Type': 'Application',
'FullName': args.full_name,
'Name': args.name,
'Description': args.description,
'Author': args.author,
'Tags': args.tags
}
return manifest
def prepare_package(args):
"""Compose required files for murano application package.
:param args: list of command line arguments
:returns: absolute path to directory with prepared files
"""
manifest = generate_manifest(args)
temp_dir = tempfile.mkdtemp()
manifest_file = os.path.join(temp_dir, 'manifest.yaml')
template_file = os.path.join(temp_dir, 'template.yaml')
logo_file = os.path.join(temp_dir, 'logo.png')
if not args.logo:
shutil.copyfile(muranoclient.get_resource('heatlogo.png'), logo_file)
else:
if os.path.isfile(args.logo):
shutil.copyfile(args.logo, logo_file)
with open(manifest_file, 'w') as f:
f.write(yaml.dump(manifest, default_flow_style=False))
shutil.copyfile(args.template, template_file)
return temp_dir

View File

@ -0,0 +1,214 @@
# Copyright (c) 2014 Mirantis, Inc.
#
# 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 shutil
import tempfile
import yaml
from muranoclient.common import utils
from muranoclient.openstack.common.apiclient import exceptions
def prepare_package(args):
"""Prepare all files and directories for that application package.
Generates manifest file and all required parameters for that.
:param args: list of command line arguments
:returns: absolute path to directory with prepared files
"""
if args.type and args.type not in ['Application', 'Library']:
raise exceptions.CommandError(
"--type should be set to 'Application' or 'Library'")
manifest = generate_manifest(args)
if args.type == 'Application':
if not args.ui:
raise exceptions.CommandError("'--ui' is required parameter")
if not os.path.exists(args.ui) or not os.path.isfile(args.ui):
raise exceptions.CommandError(
"{0} is not a file or doesn`t exist".format(args.ui))
temp_dir = tempfile.mkdtemp()
manifest_file = os.path.join(temp_dir, 'manifest.yaml')
classes_directory = os.path.join(temp_dir, 'Classes')
resource_directory = os.path.join(temp_dir, 'Resources')
with open(manifest_file, 'w') as f:
f.write(yaml.dump(manifest, default_flow_style=False))
if args.logo and os.path.isfile(args.logo):
logo_file = os.path.join(temp_dir, 'logo.png')
shutil.copyfile(args.logo, logo_file)
shutil.copytree(args.classes_dir, classes_directory)
shutil.copytree(args.resources_dir, resource_directory)
if args.ui:
ui_directory = os.path.join(temp_dir, 'UI')
os.mkdir(ui_directory)
shutil.copyfile(args.ui, os.path.join(ui_directory, 'ui.yaml'))
return temp_dir
def generate_manifest(args):
"""Generates application manifest file.
If some parameters are missed - they we be generated automatically.
:param args:
:returns: dictionary, contains manifest file data
"""
if not os.path.isdir(args.classes_dir):
raise exceptions.CommandError(
"'--classes-dir' parameter should be a directory")
if not os.path.isdir(args.resources_dir):
raise exceptions.CommandError(
"'--resources-dir' parameter should be a directory")
args = update_args(args)
if not args.type:
raise exceptions.CommandError(
"Too few arguments: --type and --full-name is required")
if not args.author:
args.author = args.os_username
if not args.description:
args.description = "Description for the application is not provided"
if not args.full_name:
raise exceptions.CommandError(
"Please, provide --full-name parameter")
manifest = {
'Format': 'MuranoPL/1.0',
'Type': args.type,
'FullName': args.full_name,
'Name': args.name,
'Description': args.description,
'Author': args.author,
'Classes': args.classes
}
if args.tags:
manifest['Tags'] = args.tags
return manifest
def update_args(args):
"""Add and update arguments if possible.
Some parameters are not required and would be guessed
from muranoPL classes: thus, if class extends system application class
fully qualified and require names could be calculated.
Also, in that case type of a package could be set to 'Application'.
"""
classes = {}
extends_from_application = False
for root, dirs, files in os.walk(args.classes_dir):
for class_file in files:
class_file_path = os.path.join(root, class_file)
try:
with open(class_file_path) as f:
content = yaml.load(f, utils.YaqlYamlLoader)
if not content.get('Name'):
raise exceptions.CommandError(
"Error in class definition: 'Name' "
"section is required")
class_name = get_fqn_for_name(content.get('Namespaces'),
content['Name'])
if root == args.classes_dir:
relative_path = class_file
else:
relative_path = os.path.join(
root.replace(args.classes_dir, "")[1:],
class_file)
classes[class_name] = relative_path
extends_from_application = check_derived_from_application(
content, extends_from_application)
if extends_from_application:
if not args.type:
args.type = 'Application'
if not args.name:
args.name = class_name.split('.')[-1]
if not args.full_name:
args.full_name = class_name
except yaml.YAMLError:
raise exceptions.CommandError(
"MuranoPL class {0} should be"
" a valid yaml file".format(class_file_path))
except IOError:
raise exceptions.CommandError(
"Could not open file {0}".format(class_file_path))
if not classes:
raise exceptions.CommandError("Application should have "
"at least one class")
args.classes = classes
return args
def get_fqn_for_name(namespaces, name):
"""Analyze name for namespace reference.
If namespaces are used - return a full name
:param namespaces: content of 'Namespaces' section of muranoPL class
:param name: name that should be checked
:returns: generated name according to namespaces
"""
values = name.split(':')
if len(values) == 1:
if '=' in namespaces:
return namespaces['='] + '.' + values[0]
return values[0]
if len(values) > 2:
raise exceptions.CommandError(
"Error in class definition: Wrong usage of ':' is "
"reserved for namespace referencing and could "
"be used only once "
"for each name")
if not namespaces:
raise exceptions.CommandError(
"Error in {0} class definition: "
"'Namespaces' section is missed")
result = namespaces.get(values[0])
if not result:
raise exceptions.CommandError(
"Error in class definition: namespaces "
"reference is not correct at the 'Extends'"
" section")
return result + '.' + values[1]
def check_derived_from_application(content, extends_from_application):
"""Look up for system 'io.murano.Application'
class in extends section.
"""
if content.get('Extends'):
extends = content['Extends']
if not isinstance(extends, list):
extends = [extends]
for name in extends:
parent_class_name = get_fqn_for_name(
content.get('Namespaces'),
name)
if parent_class_name == 'io.murano.Application':
if not extends_from_application:
return True
else:
raise exceptions.CommandError(
"Murano package should have only one class"
" extends 'io.murano.Application' class")
return False

View File

@ -12,10 +12,16 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import os
import shutil
import sys import sys
import tempfile
import zipfile
from muranoclient.common import exceptions
from muranoclient.common import utils from muranoclient.common import utils
from muranoclient.openstack.common.apiclient import exceptions
from muranoclient.v1.package_creator import hot_package
from muranoclient.v1.package_creator import mpl_package
def do_environment_list(mc, args={}): def do_environment_list(mc, args={}):
@ -75,7 +81,7 @@ def do_environment_show(mc, args):
utils.print_dict(environment.to_dict(), formatters=formatters) utils.print_dict(environment.to_dict(), formatters=formatters)
@utils.arg("environment_id", @utils.arg("environment-id",
help="Environment id for which to list deployments") help="Environment id for which to list deployments")
def do_deployment_list(mc, args): def do_deployment_list(mc, args):
"""List deployments for an environment.""" """List deployments for an environment."""
@ -107,12 +113,13 @@ def do_package_list(mc, args={}):
utils.print_list(packages, fields, field_labels, sortby=0) utils.print_list(packages, fields, field_labels, sortby=0)
@utils.arg("package_id", @utils.arg("package-id",
help="Package ID to download") help="Package ID to download")
@utils.arg("filename", metavar="file", nargs="?", @utils.arg("filename", metavar="file", nargs="?",
help="Filename for download (defaults to stdout)") help="Filename for download (defaults to stdout)")
def do_package_download(mc, args): def do_package_download(mc, args):
"""Download a package to a filename or stdout.""" """Download a package to a filename or stdout."""
def download_to_fh(package_id, fh): def download_to_fh(package_id, fh):
fh.write(mc.packages.download(package_id)) fh.write(mc.packages.download(package_id))
@ -127,7 +134,7 @@ def do_package_download(mc, args):
raise exceptions.CommandError("Package %s not found" % args.package_id) raise exceptions.CommandError("Package %s not found" % args.package_id)
@utils.arg("package_id", @utils.arg("package-id",
help="Package ID to show") help="Package ID to show")
def do_package_show(mc, args): def do_package_show(mc, args):
"""Display details for a package.""" """Display details for a package."""
@ -158,7 +165,7 @@ def do_package_show(mc, args):
utils.print_dict(to_display, formatters) utils.print_dict(to_display, formatters)
@utils.arg("package_id", @utils.arg("package-id",
help="Package ID to delete") help="Package ID to delete")
def do_package_delete(mc, args): def do_package_delete(mc, args):
"""Delete a package.""" """Delete a package."""
@ -190,3 +197,62 @@ def do_service_show(mc, args):
type=getattr(service, '?')['type'] type=getattr(service, '?')['type']
) )
utils.print_dict(to_display) utils.print_dict(to_display)
@utils.arg('-t', '--template', metavar='<HEAT_TEMPLATE>',
help='Path to the Heat template to import as '
'an Application Definition')
@utils.arg('-c', '--classes-dir', metavar='<CLASSES_DIRECTORY>',
help='Path to the directory containing application classes')
@utils.arg('-r', '--resources-dir', metavar='<RESOURCES_DIRECTORY>',
help='Path to the directory containing application resources')
@utils.arg('-n', '--name', metavar='<DISPLAY_NAME>',
help='Display name of the Application in Catalog')
@utils.arg('-f', '--full-name', metavar='<full-name>',
help='Fully-qualified name of the Application in Catalog')
@utils.arg('-a', '--author', metavar='<AUTHOR>', help='Name of the publisher')
@utils.arg('--tags', help='List of keywords connected to the application',
metavar='<TAG1 TAG2>', nargs='*')
@utils.arg('-d', '--description', metavar='<DESCRIPTION>',
help='Detailed description for the Application in Catalog')
@utils.arg('-o', '--output', metavar='<PACKAGE_NAME>',
help='The name of the output file archive to save locally')
@utils.arg('-u', '--ui', metavar='<UI_DEFINITION>',
help='Dynamic UI form definition')
@utils.arg('--type',
help='Package type. Possible values: Application or Library')
@utils.arg('-l', '--logo', metavar='<LOGO>', help='Path to the package logo')
def do_package_create(mc, args):
"""Create an application package."""
if args.template and (args.classes_dir or args.resources_dir):
raise exceptions.CommandError(
"Provide --template for a HOT-based package, OR --classes-dir"
" and --resources-dir for a MuranoPL-based package")
if not args.template and (not args.classes_dir or not args.resources_dir):
raise exceptions.CommandError(
"Provide --template for a HOT-based package, OR --classes-dir"
" and --resources-dir for a MuranoPL-based package")
directory_path = None
try:
if args.template:
directory_path = hot_package.prepare_package(args)
else:
directory_path = mpl_package.prepare_package(args)
archive_name = args.output or tempfile.mktemp(prefix="murano_")
_make_archive(archive_name, directory_path)
print("Application package is available at " +
os.path.abspath(archive_name))
finally:
if directory_path:
shutil.rmtree(directory_path)
def _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))

View File

@ -9,3 +9,4 @@ Babel>=1.3
pyOpenSSL>=0.11 pyOpenSSL>=0.11
requests>=1.1 requests>=1.1
PyYAML>=3.1.0 PyYAML>=3.1.0
yaql>=0.2.2,<0.3