diff --git a/requirements.txt b/requirements.txt index b9792daf..391f84a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,7 @@ PyYAML>=3.1.0 # MIT python-dateutil>=2.4.2 # BSD six>=1.9.0 # MIT tosca-parser>=0.5.0 # Apache-2.0 +keystoneauth1>=2.7.0 # Apache-2.0 +python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0 +python-heatclient>=1.1.0 # Apache-2.0 +python-glanceclient>=2.0.0 # Apache-2.0 diff --git a/translator/common/flavors.py b/translator/common/flavors.py new file mode 100644 index 00000000..543529dd --- /dev/null +++ b/translator/common/flavors.py @@ -0,0 +1,54 @@ +# 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 logging + +# NOTE(aloga): this should be safe. If we do not have the clients, we won't +# have the session below, therefore the clients won't be ever called. +try: + import novaclient.client +except ImportError: + pass + +log = logging.getLogger('heat-translator') + + +_FLAVORS = { + 'm1.xlarge': {'mem_size': 16384, 'disk_size': 160, 'num_cpus': 8}, + 'm1.large': {'mem_size': 8192, 'disk_size': 80, 'num_cpus': 4}, + 'm1.medium': {'mem_size': 4096, 'disk_size': 40, 'num_cpus': 2}, + 'm1.small': {'mem_size': 2048, 'disk_size': 20, 'num_cpus': 1}, + 'm1.tiny': {'mem_size': 512, 'disk_size': 1, 'num_cpus': 1}, + 'm1.micro': {'mem_size': 128, 'disk_size': 0, 'num_cpus': 1}, + 'm1.nano': {'mem_size': 64, 'disk_size': 0, 'num_cpus': 1} +} + +SESSION = None + + +def get_flavors(): + ret = {} + if SESSION is not None: + try: + client = novaclient.client.Client("2", session=SESSION) + except Exception as e: + # Handles any exception coming from openstack + log.warn(_('Choosing predefined flavors since received ' + 'Openstack Exception: %s') % str(e)) + else: + for flv in client.flavors.list(detailed=True): + ret[str(flv.name)] = { + "mem_size": flv.ram, + "disk_size": flv.disk, + "num_cpus": flv.vcpus + } + return ret or _FLAVORS diff --git a/translator/common/images.py b/translator/common/images.py new file mode 100644 index 00000000..9cebcef8 --- /dev/null +++ b/translator/common/images.py @@ -0,0 +1,81 @@ +# 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 logging + +# NOTE(aloga): this should be safe. If we do not have the clients, we won't +# have the session below, therefore the clients won't be ever called. +try: + import glanceclient.client +except ImportError: + pass + +log = logging.getLogger('heat-translator') + + +_IMAGES = { + 'ubuntu-software-config-os-init': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'Ubuntu', + 'version': '14.04'}, + 'ubuntu-12.04-software-config-os-init': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'Ubuntu', + 'version': '12.04'}, + 'fedora-amd64-heat-config': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'Fedora', + 'version': '18.0'}, + 'F18-x86_64-cfntools': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'Fedora', + 'version': '19'}, + 'Fedora-x86_64-20-20131211.1-sda': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'Fedora', + 'version': '20'}, + 'cirros-0.3.1-x86_64-uec': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'CirrOS', + 'version': '0.3.1'}, + 'cirros-0.3.2-x86_64-uec': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'CirrOS', + 'version': '0.3.2'}, + 'rhel-6.5-test-image': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'RHEL', + 'version': '6.5'} +} + +SESSION = None + + +def get_images(): + ret = {} + + if SESSION is not None: + try: + client = glanceclient.client.Client("2", session=SESSION) + except Exception as e: + # Handles any exception coming from openstack + log.warn(_('Choosing predefined images since received ' + 'Openstack Exception: %s') % str(e)) + else: + for image in client.images.list(): + metadata = ["architecture", "type", "distribution", "version"] + if any(key in image.keys() for key in metadata): + ret = [image["name"]] = {} + for key in metadata: + if key in image.keys(): + ret[image["name"]][key] = image[key] + return ret or _IMAGES diff --git a/translator/hot/tosca/tests/test_tosca_compute.py b/translator/hot/tosca/tests/test_tosca_compute.py index d42cdc8f..f31beb5c 100644 --- a/translator/hot/tosca/tests/test_tosca_compute.py +++ b/translator/hot/tosca/tests/test_tosca_compute.py @@ -10,9 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import json import mock -from mock import patch from toscaparser.nodetemplate import NodeTemplate from toscaparser.tests.base import TestCase @@ -198,11 +196,8 @@ class ToscaComputeTest(TestCase): tpl_snippet, expectedprops) - @patch('requests.post') - @patch('requests.get') - @patch('os.getenv') - def test_node_compute_with_nova_flavor(self, mock_os_getenv, - mock_get, mock_post): + @mock.patch('translator.common.flavors.get_flavors') + def test_node_compute_with_nova_flavor(self, mock_flavor): tpl_snippet = ''' node_templates: server: @@ -214,55 +209,19 @@ class ToscaComputeTest(TestCase): disk_size: 1 GB mem_size: 1 GB ''' - with patch('translator.common.utils.' - 'check_for_env_variables') as mock_check_env: - mock_check_env.return_value = True - mock_os_getenv.side_effect = ['demo', 'demo', - 'demo', 'http://abc.com/5000/', - 'demo', 'demo', - 'demo', 'http://abc.com/5000/'] - mock_ks_response = mock.MagicMock() - mock_ks_response.status_code = 200 - mock_ks_content = { - 'access': { - 'token': { - 'id': 'd1dfa603-3662-47e0-b0b6-3ae7914bdf76' - }, - 'serviceCatalog': [{ - 'type': 'compute', - 'endpoints': [{ - 'publicURL': 'http://abc.com' - }] - }] - } - } - mock_ks_response.content = json.dumps(mock_ks_content) - mock_nova_response = mock.MagicMock() - mock_nova_response.status_code = 200 - mock_flavor_content = { - 'flavors': [{ - 'name': 'm1.mock_flavor', - 'ram': 1024, - 'disk': 1, - 'vcpus': 1 - }] - } - mock_nova_response.content = \ - json.dumps(mock_flavor_content) - mock_post.return_value = mock_ks_response - mock_get.return_value = mock_nova_response - expectedprops = {'flavor': 'm1.mock_flavor', - 'image': None, - 'user_data_format': 'SOFTWARE_CONFIG'} - self._tosca_compute_test( - tpl_snippet, - expectedprops) + mock_flavor.return_value = { + 'm1.mock_flavor': { + 'mem_size': 1024, + 'disk_size': 1, + 'num_cpus': 1} + } + expectedprops = {'flavor': 'm1.mock_flavor', + 'image': None, + 'user_data_format': 'SOFTWARE_CONFIG'} + self._tosca_compute_test(tpl_snippet, expectedprops) - @patch('requests.post') - @patch('requests.get') - @patch('os.getenv') - def test_node_compute_without_nova_flavor(self, mock_os_getenv, - mock_get, mock_post): + @mock.patch('translator.common.images.get_images') + def test_node_compute_with_glance_image(self, mock_images): tpl_snippet = ''' node_templates: server: @@ -273,18 +232,24 @@ class ToscaComputeTest(TestCase): num_cpus: 1 disk_size: 1 GB mem_size: 1 GB + os: + properties: + architecture: x86_64 + type: Linux + distribution: Fake Distribution + version: 19.0 ''' - with patch('translator.common.utils.' - 'check_for_env_variables') as mock_check_env: - mock_check_env.return_value = True - mock_os_getenv.side_effect = ['demo', 'demo', - 'demo', 'http://abc.com/5000/'] - mock_ks_response = mock.MagicMock() - mock_ks_content = {} - mock_ks_response.content = json.dumps(mock_ks_content) - expectedprops = {'flavor': 'm1.small', - 'image': None, - 'user_data_format': 'SOFTWARE_CONFIG'} - self._tosca_compute_test( - tpl_snippet, - expectedprops) + mock_images.return_value = { + 'fake-image-foobar': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'Fake Distribution', + 'version': '19.0'}, + 'fake-image-foobar-old': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'Fake Distribution', + 'version': '18.0'} + } + expectedprops = {'flavor': 'm1.small', + 'image': 'fake-image-foobar', + 'user_data_format': 'SOFTWARE_CONFIG'} + self._tosca_compute_test(tpl_snippet, expectedprops) diff --git a/translator/hot/tosca/tosca_compute.py b/translator/hot/tosca/tosca_compute.py index b8ad83ca..1e4f061c 100755 --- a/translator/hot/tosca/tosca_compute.py +++ b/translator/hot/tosca/tosca_compute.py @@ -11,68 +11,21 @@ # License for the specific language governing permissions and limitations # under the License. -import json import logging -import requests from toscaparser.utils.gettextutils import _ +from translator.common import flavors as nova_flavors +from translator.common import images as glance_images import translator.common.utils from translator.hot.syntax.hot_resource import HotResource + log = logging.getLogger('heat-translator') # Name used to dynamically load appropriate map class. TARGET_CLASS_NAME = 'ToscaCompute' -# A design issue to be resolved is how to translate the generic TOSCA server -# properties to OpenStack flavors and images. At the Atlanta design summit, -# there was discussion on using Glance to store metadata and Graffiti to -# describe artifacts. We will follow these projects to see if they can be -# leveraged for this TOSCA translation. -# For development purpose at this time, we temporarily hardcode a list of -# flavors and images here -FLAVORS = {'m1.xlarge': {'mem_size': 16384, 'disk_size': 160, 'num_cpus': 8}, - 'm1.large': {'mem_size': 8192, 'disk_size': 80, 'num_cpus': 4}, - 'm1.medium': {'mem_size': 4096, 'disk_size': 40, 'num_cpus': 2}, - 'm1.small': {'mem_size': 2048, 'disk_size': 20, 'num_cpus': 1}, - 'm1.tiny': {'mem_size': 512, 'disk_size': 1, 'num_cpus': 1}, - 'm1.micro': {'mem_size': 128, 'disk_size': 0, 'num_cpus': 1}, - 'm1.nano': {'mem_size': 64, 'disk_size': 0, 'num_cpus': 1}} - -IMAGES = {'ubuntu-software-config-os-init': {'architecture': 'x86_64', - 'type': 'Linux', - 'distribution': 'Ubuntu', - 'version': '14.04'}, - 'ubuntu-12.04-software-config-os-init': {'architecture': 'x86_64', - 'type': 'Linux', - 'distribution': 'Ubuntu', - 'version': '12.04'}, - 'fedora-amd64-heat-config': {'architecture': 'x86_64', - 'type': 'Linux', - 'distribution': 'Fedora', - 'version': '18.0'}, - 'F18-x86_64-cfntools': {'architecture': 'x86_64', - 'type': 'Linux', - 'distribution': 'Fedora', - 'version': '19'}, - 'Fedora-x86_64-20-20131211.1-sda': {'architecture': 'x86_64', - 'type': 'Linux', - 'distribution': 'Fedora', - 'version': '20'}, - 'cirros-0.3.1-x86_64-uec': {'architecture': 'x86_64', - 'type': 'Linux', - 'distribution': 'CirrOS', - 'version': '0.3.1'}, - 'cirros-0.3.2-x86_64-uec': {'architecture': 'x86_64', - 'type': 'Linux', - 'distribution': 'CirrOS', - 'version': '0.3.2'}, - 'rhel-6.5-test-image': {'architecture': 'x86_64', - 'type': 'Linux', - 'distribution': 'RHEL', - 'version': '6.5'}} - class ToscaCompute(HotResource): '''Translate TOSCA node type tosca.nodes.Compute.''' @@ -136,88 +89,10 @@ class ToscaCompute(HotResource): hot_properties['image'] = image return hot_properties - def _create_nova_flavor_dict(self): - '''Populates and returns the flavors dict using Nova ReST API''' - try: - access_dict = translator.common.utils.get_ks_access_dict() - access_token = translator.common.utils.get_token_id(access_dict) - if access_token is None: - return None - nova_url = translator.common.utils.get_url_for(access_dict, - 'compute') - if not nova_url: - return None - nova_response = requests.get(nova_url + '/flavors/detail', - headers={'X-Auth-Token': - access_token}) - if nova_response.status_code != 200: - return None - flavors = json.loads(nova_response.content)['flavors'] - flavor_dict = dict() - for flavor in flavors: - flavor_name = str(flavor['name']) - flavor_dict[flavor_name] = { - 'mem_size': flavor['ram'], - 'disk_size': flavor['disk'], - 'num_cpus': flavor['vcpus'], - } - except Exception as e: - # Handles any exception coming from openstack - log.warn(_('Choosing predefined flavors since received ' - 'Openstack Exception: %s') % str(e)) - return None - return flavor_dict - - def _populate_image_dict(self): - '''Populates and returns the images dict using Glance ReST API''' - images_dict = {} - try: - access_dict = translator.common.utils.get_ks_access_dict() - access_token = translator.common.utils.get_token_id(access_dict) - if access_token is None: - return None - glance_url = translator.common.utils.get_url_for(access_dict, - 'image') - if not glance_url: - return None - glance_response = requests.get(glance_url + '/v2/images', - headers={'X-Auth-Token': - access_token}) - if glance_response.status_code != 200: - return None - images = json.loads(glance_response.content)["images"] - for image in images: - image_resp = requests.get(glance_url + '/v2/images/' + - image["id"], - headers={'X-Auth-Token': - access_token}) - if image_resp.status_code != 200: - continue - metadata = ["architecture", "type", "distribution", "version"] - image_data = json.loads(image_resp.content) - if any(key in image_data.keys() for key in metadata): - images_dict[image_data["name"]] = dict() - for key in metadata: - if key in image_data.keys(): - images_dict[image_data["name"]][key] = \ - image_data[key] - else: - continue - - except Exception as e: - # Handles any exception coming from openstack - log.warn(_('Choosing predefined flavors since received ' - 'Openstack Exception: %s') % str(e)) - return images_dict - def _best_flavor(self, properties): log.info(_('Choosing the best flavor for given attributes.')) # Check whether user exported all required environment variables. - flavors = FLAVORS - if translator.common.utils.check_for_env_variables(): - resp = self._create_nova_flavor_dict() - if resp: - flavors = resp + flavors = nova_flavors.get_flavors() # start with all flavors match_all = flavors.keys() @@ -260,11 +135,7 @@ class ToscaCompute(HotResource): def _best_image(self, properties): # Check whether user exported all required environment variables. - images = IMAGES - if translator.common.utils.check_for_env_variables(): - resp = self._populate_image_dict() - if len(resp.keys()) > 0: - images = resp + images = glance_images.get_images() match_all = images.keys() architecture = properties.get(self.ARCHITECTURE) if architecture is None: diff --git a/translator/osc/v1/tests/fakes.py b/translator/osc/v1/tests/fakes.py index a08c3ace..3bab0b76 100644 --- a/translator/osc/v1/tests/fakes.py +++ b/translator/osc/v1/tests/fakes.py @@ -12,6 +12,8 @@ import sys +import mock + class FakeApp(object): def __init__(self): @@ -20,6 +22,9 @@ class FakeApp(object): self.stdout = sys.stdout self.stderr = sys.stderr + self.cloud = mock.Mock() + self.cloud.get_session.return_value = None + class FakeClientManager(object): def __init__(self): diff --git a/translator/osc/v1/translate.py b/translator/osc/v1/translate.py index ef005e2d..eb4de851 100644 --- a/translator/osc/v1/translate.py +++ b/translator/osc/v1/translate.py @@ -21,6 +21,8 @@ from cliff import command from toscaparser.tosca_template import ToscaTemplate from toscaparser.utils.gettextutils import _ +from translator.common import flavors +from translator.common import images from translator.common.utils import UrlUtils from translator.conf.config import ConfigProvider from translator.hot.tosca_translator import TOSCATranslator @@ -35,7 +37,7 @@ class TranslateTemplate(command.Command): """Translate a template""" - auth_required = False + auth_required = True def get_parser(self, prog_name): parser = super(TranslateTemplate, self).get_parser(prog_name) @@ -73,6 +75,10 @@ class TranslateTemplate(command.Command): '(%s).'), parsed_args) output = None + session = self.app.cloud.get_session() + flavors.SESSION = session + images.SESSION = session + if parsed_args.parameter: parsed_params = parsed_args.parameter else: diff --git a/translator/shell.py b/translator/shell.py index d5333bc0..a11d66fd 100644 --- a/translator/shell.py +++ b/translator/shell.py @@ -12,21 +12,28 @@ import argparse -import ast -import json import logging import logging.config import os -import prettytable -import requests import sys import uuid import yaml +# NOTE(aloga): As per upstream developers requirement this needs to work +# without the clients, therefore we need to pass if we cannot import them +try: + import heatclient.client + from keystoneauth1 import loading +except ImportError: + has_clients = False +else: + has_clients = True + from toscaparser.tosca_template import ToscaTemplate from toscaparser.utils.gettextutils import _ from toscaparser.utils.urlutils import UrlUtils -from translator.common import utils +from translator.common import flavors +from translator.common import images from translator.conf.config import ConfigProvider from translator.hot.tosca_translator import TOSCATranslator @@ -55,7 +62,7 @@ class TranslatorShell(object): SUPPORTED_TYPES = ['tosca'] - def get_parser(self): + def get_parser(self, argv): parser = argparse.ArgumentParser(prog="heat-translator") parser.add_argument('--template-file', @@ -91,11 +98,25 @@ class TranslatorShell(object): help=_('Whether to deploy the generated template ' 'or not.')) + self._append_global_identity_args(parser, argv) + return parser + def _append_global_identity_args(self, parser, argv): + if not has_clients: + return + + loading.register_session_argparse_arguments(parser) + + default_auth_plugin = 'password' + if 'os-token' in argv: + default_auth_plugin = 'token' + loading.register_auth_argparse_arguments( + parser, argv, default=default_auth_plugin) + def main(self, argv): - parser = self.get_parser() + parser = self.get_parser(argv) (args, args_list) = parser.parse_known_args(argv) template_file = args.template_file @@ -117,16 +138,28 @@ class TranslatorShell(object): 'validation.') % {'template_file': template_file}) print(msg) else: - heat_tpl = self._translate(template_type, template_file, - parsed_params, a_file, deploy) - if heat_tpl: - if utils.check_for_env_variables() and deploy: - try: - heatclient(heat_tpl, parsed_params) - except Exception: - log.error(_("Unable to launch the heat stack")) + hot = self._translate(template_type, template_file, + parsed_params, a_file, deploy) + if hot and deploy: + if not has_clients: + raise RuntimeError(_('Could not find OpenStack ' + 'clients and libs, aborting ')) - self._write_output(heat_tpl, output_file) + keystone_auth = ( + loading.load_auth_from_argparse_arguments(args) + ) + keystone_session = ( + loading.load_session_from_argparse_arguments( + args, + auth=keystone_auth + ) + ) + images.SESSION = keystone_session + flavors.SESSION = keystone_session + self.deploy_on_heat(keystone_session, keystone_auth, + hot, parsed_params) + + self._write_output(hot, output_file) else: msg = (_('The path %(template_file)s is not a valid ' 'file or URL.') % {'template_file': template_file}) @@ -134,6 +167,20 @@ class TranslatorShell(object): log.error(msg) raise ValueError(msg) + def deploy_on_heat(self, session, auth, template, parameters): + endpoint = auth.get_endpoint(session, service_type="orchestration") + client = heatclient.client.Client('1', + session=session, + auth=auth, + endpoint=endpoint) + + stack_name = "heat_" + str(uuid.uuid4()).split("-")[0] + tpl = yaml.load(template) + tpl['heat_template_version'] = str(tpl['heat_template_version']) + client.stacks.create(stack_name=stack_name, + template=tpl, + parameters=parameters) + def _parse_parameters(self, parameter_list): parsed_inputs = {} @@ -177,48 +224,6 @@ class TranslatorShell(object): print(output) -def heatclient(output, params): - try: - access_dict = utils.get_ks_access_dict() - endpoint = utils.get_url_for(access_dict, 'orchestration') - token = utils.get_token_id(access_dict) - except Exception as e: - log.error(e) - headers = { - 'Content-Type': 'application/json', - 'X-Auth-Token': token - } - heat_stack_name = "heat_" + str(uuid.uuid4()).split("-")[0] - output = yaml.load(output) - output['heat_template_version'] = str(output['heat_template_version']) - data = { - 'stack_name': heat_stack_name, - 'template': output, - 'parameters': params - } - response = requests.post(endpoint + '/stacks', - data=json.dumps(data), - headers=headers) - content = ast.literal_eval(response._content) - if response.status_code == 201: - stack_id = content["stack"]["id"] - get_url = endpoint + '/stacks/' + heat_stack_name + '/' + stack_id - get_stack_response = requests.get(get_url, - headers=headers) - stack_details = json.loads(get_stack_response.content)["stack"] - col_names = ["id", "stack_name", "stack_status", "creation_time", - "updated_time"] - pt = prettytable.PrettyTable(col_names) - stack_list = [] - for col in col_names: - stack_list.append(stack_details[col]) - pt.add_row(stack_list) - print(pt) - else: - err_msg = content["error"]["message"] - log(_("Unable to deploy to Heat\n%s\n") % err_msg) - - def main(args=None): if args is None: args = sys.argv[1:] diff --git a/translator/tests/test_shell.py b/translator/tests/test_shell.py index 62f35100..7f8e11ed 100644 --- a/translator/tests/test_shell.py +++ b/translator/tests/test_shell.py @@ -10,13 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import ast -import json import os import shutil import tempfile -from mock import patch from toscaparser.common import exception from toscaparser.utils.gettextutils import _ import translator.shell as shell @@ -49,10 +46,7 @@ class ShellTest(TestCase): '--parameters=key')) def test_valid_template(self): - try: - shell.main([self.template_file, self.template_type]) - except Exception: - self.fail(self.failure_msg) + shell.main([self.template_file, self.template_type]) def test_valid_template_without_type(self): try: @@ -100,86 +94,3 @@ class ShellTest(TestCase): shutil.rmtree(temp_dir) self.assertTrue(temp_dir is None or not os.path.exists(temp_dir)) - - @patch('uuid.uuid4') - @patch('translator.common.utils.check_for_env_variables') - @patch('requests.post') - @patch('translator.common.utils.get_url_for') - @patch('translator.common.utils.get_token_id') - @patch('os.getenv') - @patch('translator.hot.tosca.tosca_compute.' - 'ToscaCompute._create_nova_flavor_dict') - @patch('translator.hot.tosca.tosca_compute.' - 'ToscaCompute._populate_image_dict') - def test_template_deploy_with_credentials(self, mock_populate_image_dict, - mock_flavor_dict, - mock_os_getenv, - mock_token, - mock_url, mock_post, - mock_env, - mock_uuid): - mock_uuid.return_value = 'abcXXX-abcXXX' - mock_env.return_value = True - mock_flavor_dict.return_value = { - 'm1.medium': {'mem_size': 4096, 'disk_size': 40, 'num_cpus': 2} - } - mock_populate_image_dict.return_value = { - "rhel-6.5-test-image": { - "version": "6.5", - "architecture": "x86_64", - "distribution": "RHEL", - "type": "Linux" - } - } - mock_url.return_value = 'http://abc.com' - mock_token.return_value = 'mock_token' - mock_os_getenv.side_effect = ['demo', 'demo', - 'demo', 'http://www.abc.com'] - try: - data = { - 'stack_name': 'heat_abcXXX', - 'parameters': {}, - 'template': { - 'outputs': {}, - 'heat_template_version': '2013-05-23', - 'description': 'Template for deploying a single server ' - 'with predefined properties.\n', - 'parameters': {}, - 'resources': { - 'my_server': { - 'type': 'OS::Nova::Server', - 'properties': { - 'flavor': 'm1.medium', - 'user_data_format': 'SOFTWARE_CONFIG', - 'image': 'rhel-6.5-test-image' - } - } - } - } - } - - mock_heat_res = { - "stack": { - "id": 1234 - } - } - headers = { - 'Content-Type': 'application/json', - 'X-Auth-Token': 'mock_token' - } - - class mock_response(object): - def __init__(self, status_code, _content): - self.status_code = status_code - self._content = _content - - mock_response_obj = mock_response(201, json.dumps(mock_heat_res)) - mock_post.return_value = mock_response_obj - shell.main([self.template_file, self.template_type, - "--deploy"]) - args, kwargs = mock_post.call_args - self.assertEqual(args[0], 'http://abc.com/stacks') - self.assertEqual(ast.literal_eval(kwargs['data']), data) - self.assertEqual(kwargs['headers'], headers) - except Exception: - self.fail(self.failure_msg)