diff --git a/translator/common/utils.py b/translator/common/utils.py index 79d98478..459b5ee8 100644 --- a/translator/common/utils.py +++ b/translator/common/utils.py @@ -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'] diff --git a/translator/hot/tosca/tests/test_tosca_compute.py b/translator/hot/tosca/tests/test_tosca_compute.py index fb8fe918..e0cdbb65 100644 --- a/translator/hot/tosca/tests/test_tosca_compute.py +++ b/translator/hot/tosca/tests/test_tosca_compute.py @@ -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/'] diff --git a/translator/hot/tosca/tosca_compute.py b/translator/hot/tosca/tosca_compute.py index a76693f6..e2ac130d 100755 --- a/translator/hot/tosca/tosca_compute.py +++ b/translator/hot/tosca/tosca_compute.py @@ -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 diff --git a/translator/shell.py b/translator/shell.py index 7ed1393b..92d92d96 100644 --- a/translator/shell.py +++ b/translator/shell.py @@ -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:] diff --git a/translator/tests/data/tosca_single_server.yaml b/translator/tests/data/tosca_single_server.yaml index c4cce9d7..67a01619 100644 --- a/translator/tests/data/tosca_single_server.yaml +++ b/translator/tests/data/tosca_single_server.yaml @@ -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] } \ No newline at end of file + value: { get_attribute: [my_server, private_address] } diff --git a/translator/tests/test_shell.py b/translator/tests/test_shell.py index abe8f5e7..b001c1a9 100644 --- a/translator/tests/test_shell.py +++ b/translator/tests/test_shell.py @@ -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)