Add support to create heat stack from TOSCA
Change-Id: Iad5b0d43924677a26e38e013c7fd66718a554b17 Implements: blueprint stack-create-translated-template
This commit is contained in:
parent
3c7baa44fd
commit
393a377265
@ -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']
|
||||
|
@ -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/']
|
||||
|
@ -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
|
||||
|
@ -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:]
|
||||
|
@ -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] }
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user