Add support to create heat stack from TOSCA

Change-Id: Iad5b0d43924677a26e38e013c7fd66718a554b17
Implements: blueprint stack-create-translated-template
This commit is contained in:
Bharath Thiruveedula 2016-02-16 07:59:09 +05:30
parent 3c7baa44fd
commit 393a377265
6 changed files with 199 additions and 41 deletions

View File

@ -11,11 +11,13 @@
# under the License.
import json
import logging
import math
import numbers
import os
import re
import requests
from six.moves.urllib.parse import urlparse
import yaml
@ -25,6 +27,9 @@ import toscaparser.utils.yamlparser
YAML_ORDER_PARSER = toscaparser.utils.yamlparser.simple_ordered_parse
log = logging.getLogger('heat-translator')
# Required environment variables to create openstackclient object.
ENV_VARIABLES = ['OS_AUTH_URL', 'OS_PASSWORD', 'OS_USERNAME', 'OS_TENANT_NAME']
class MemoryUnit(object):
@ -263,3 +268,52 @@ def str_to_num(value):
return int(value)
except ValueError:
return float(value)
def check_for_env_variables():
return set(ENV_VARIABLES) < set(os.environ.keys())
def get_ks_access_dict():
tenant_name = os.getenv('OS_TENANT_NAME')
username = os.getenv('OS_USERNAME')
password = os.getenv('OS_PASSWORD')
auth_url = os.getenv('OS_AUTH_URL')
auth_dict = {
"auth": {
"tenantName": tenant_name,
"passwordCredentials": {
"username": username,
"password": password
}
}
}
headers = {'Content-Type': 'application/json'}
try:
keystone_response = requests.post(auth_url + '/tokens',
data=json.dumps(auth_dict),
headers=headers)
if keystone_response.status_code != 200:
return None
return json.loads(keystone_response.content)
except Exception:
return None
def get_url_for(access_dict, service_type):
if access_dict is None:
return None
service_catalog = access_dict['access']['serviceCatalog']
service_url = ''
for service in service_catalog:
if service['type'] == service_type:
service_url = service['endpoints'][0]['publicURL']
break
return service_url
def get_token_id(access_dict):
if access_dict is None:
return None
return access_dict['access']['token']['id']

View File

@ -212,10 +212,12 @@ class ToscaComputeTest(TestCase):
disk_size: 1 GB
mem_size: 1 GB
'''
with patch('translator.hot.tosca.tosca_compute.ToscaCompute.'
'_check_for_env_variables') as mock_check_env:
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
@ -268,8 +270,8 @@ class ToscaComputeTest(TestCase):
disk_size: 1 GB
mem_size: 1 GB
'''
with patch('translator.hot.tosca.tosca_compute.ToscaCompute.'
'_check_for_env_variables') as mock_check_env:
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/']

View File

@ -13,7 +13,6 @@
import json
import logging
import os
import requests
from toscaparser.utils.gettextutils import _
@ -26,9 +25,6 @@ log = logging.getLogger('heat-translator')
# Name used to dynamically load appropriate map class.
TARGET_CLASS_NAME = 'ToscaCompute'
# Required environment variables to create novaclient object.
ENV_VARIABLES = ['OS_AUTH_URL', 'OS_PASSWORD', 'OS_USERNAME', 'OS_TENANT_NAME']
# 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
@ -125,41 +121,15 @@ class ToscaCompute(HotResource):
hot_properties['image'] = image
return hot_properties
def _check_for_env_variables(self):
return set(ENV_VARIABLES) < set(os.environ.keys())
def _create_nova_flavor_dict(self):
'''Populates and returns the flavors dict using Nova ReST API'''
tenant_name = os.getenv('OS_TENANT_NAME')
username = os.getenv('OS_USERNAME')
password = os.getenv('OS_PASSWORD')
auth_url = os.getenv('OS_AUTH_URL')
auth_dict = {
"auth": {
"tenantName": tenant_name,
"passwordCredentials": {
"username": username,
"password": password
}
}
}
headers = {'Content-Type': 'application/json'}
try:
keystone_response = requests.post(auth_url + '/tokens',
data=json.dumps(auth_dict),
headers=headers)
if keystone_response.status_code != 200:
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
access_dict = json.loads(keystone_response.content)
access_token = access_dict['access']['token']['id']
service_catalog = access_dict['access']['serviceCatalog']
nova_url = ''
for service in service_catalog:
if service['type'] == 'compute':
nova_url = service['endpoints'][0]['publicURL']
break
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',
@ -187,7 +157,7 @@ class ToscaCompute(HotResource):
log.info(_('Choosing the best flavor for given attributes.'))
# Check whether user exported all required environment variables.
flavors = FLAVORS
if self._check_for_env_variables():
if translator.common.utils.check_for_env_variables():
resp = self._create_nova_flavor_dict()
if resp:
flavors = resp

View File

@ -11,14 +11,21 @@
# under the License.
import ast
import json
import logging
import logging.config
import os
import prettytable
import requests
import sys
import uuid
import yaml
from toscaparser.tosca_template import ToscaTemplate
from toscaparser.utils.gettextutils import _
from toscaparser.utils.urlutils import UrlUtils
from translator.common import utils
from translator.hot.tosca_translator import TOSCATranslator
"""
@ -94,6 +101,8 @@ class TranslatorShell(object):
if "--output-file=" in arg:
output = arg
output_file = output.split('--output-file=')[1]
if "--deploy" in arg:
self.deploy = True
if parameters:
parsed_params = self._parse_parameters(parameters)
a_file = os.path.isfile(path)
@ -115,6 +124,12 @@ class TranslatorShell(object):
heat_tpl = self._translate(template_type, path, parsed_params,
a_file)
if heat_tpl:
if utils.check_for_env_variables() and self.deploy:
try:
heatclient(heat_tpl, parsed_params)
except Exception:
log.error(_("Unable to launch the heat stack"))
self._write_output(heat_tpl, output_file)
else:
msg = _("The path %(path)s is not a valid file or URL.") % {
@ -171,6 +186,48 @@ 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

@ -29,4 +29,4 @@ topology_template:
outputs:
private_ip:
description: The private IP address of the deployed server instance.
value: { get_attribute: [my_server, private_address] }
value: { get_attribute: [my_server, private_address] }

View File

@ -10,10 +10,13 @@
# 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
@ -114,3 +117,75 @@ 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')
def test_template_deploy_with_credentials(self, 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_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)