use keystoneauth and OpenStack clients

The parser should not rely on the OS_* environment variables for
authentication, as it will fail with more complex setups, when using the
"clouds.yaml" configuration file or when being called through osc with
arguments instead of environment variables.

Moreover, the client should not use "requests" directly to do GETs to
access the nova flavors and glance images, but it should use the
existing clients.

When used through the python-openstackclient it is possible to force the
client to request authentication. The authentication will be handled by
the client, so that the parser does not need to worry about it (so that
authentication plugins can be used). Then, the existing authentication
session can be reused for all the client interaction with OpenStack to
fetch the flavor and images information.

Change-Id: I9f65e7d46686c37bb44ef18756ebd295ee4961de
This commit is contained in:
Alvaro Lopez Garcia 2016-06-21 16:44:14 +02:00
parent 63d6003b3d
commit 9ed0a3d134
9 changed files with 254 additions and 352 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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