diff --git a/tripleoclient/fake_keystone.py b/tripleoclient/fake_keystone.py index ab13876ad..947b43377 100644 --- a/tripleoclient/fake_keystone.py +++ b/tripleoclient/fake_keystone.py @@ -12,7 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. # -import BaseHTTPServer + +try: + import http.server as BaseHTTPServer # Python3 +except ImportError: + import BaseHTTPServer # Python2 import datetime import json import logging diff --git a/tripleoclient/tests/base.py b/tripleoclient/tests/base.py index 7f35f9eb1..d2fc571a6 100644 --- a/tripleoclient/tests/base.py +++ b/tripleoclient/tests/base.py @@ -44,7 +44,7 @@ class TestCase(testtools.TestCase): self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) self.useFixture(fixtures.NestedTempfile()) - self.useFixture(fixtures.TempHomeDir()) + self.temp_homedir = self.useFixture(fixtures.TempHomeDir()).path if os.environ.get('OS_STDOUT_CAPTURE') in _TRUE_VALUES: stdout = self.useFixture(fixtures.StringStream('stdout')).stream diff --git a/tripleoclient/tests/v1/undercloud/test_undercloud_deploy.py b/tripleoclient/tests/v1/undercloud/test_undercloud_deploy.py new file mode 100644 index 000000000..d2d19c6ca --- /dev/null +++ b/tripleoclient/tests/v1/undercloud/test_undercloud_deploy.py @@ -0,0 +1,85 @@ +# Copyright 2015 Red Hat, Inc. +# +# 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 mock +import os + +from tripleoclient.tests.v1.test_plugin import TestPluginV1 + +# Load the plugin init module for the plugin list and show commands +from tripleoclient.v1 import undercloud_deploy + + +class FakePluginV1Client(object): + def __init__(self, **kwargs): + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] + + +class TestUndercloudDeploy(TestPluginV1): + + def setUp(self): + super(TestUndercloudDeploy, self).setUp() + + # Get the command object to test + self.cmd = undercloud_deploy.DeployUndercloud(self.app, None) + + @mock.patch('os.path.exists') + @mock.patch('tripleo_common.utils.passwords.generate_passwords') + @mock.patch('yaml.safe_dump') + def test_update_passwords_env_init(self, mock_dump, mock_pw, mock_exists): + pw_dict = {"GeneratedPassword": 123} + pw_env_path = os.path.join(self.temp_homedir, + 'tripleo-undercloud-passwords.yaml') + + mock_pw.return_value = pw_dict + mock_exists.return_value = False + mock_open_context = mock.mock_open() + with mock.patch('six.moves.builtins.open', mock_open_context): + self.cmd._update_passwords_env() + + mock_exists.assert_called_once_with(pw_env_path) + mock_open_context.assert_called_once_with(pw_env_path, 'w') + mock_open_handle = mock_open_context() + mock_dump.assert_called_once_with({'parameter_defaults': pw_dict}, + mock_open_handle, + default_flow_style=False) + + @mock.patch('os.path.exists') + @mock.patch('tripleo_common.utils.passwords.generate_passwords') + @mock.patch('yaml.safe_dump') + def test_update_passwords_env_update(self, mock_dump, mock_pw, + mock_exists): + pw_dict = {"GeneratedPassword": 123} + pw_env_path = os.path.join(self.temp_homedir, + 'tripleo-undercloud-passwords.yaml') + + mock_pw.return_value = pw_dict + mock_exists.return_value = True + mock_open_context = mock.mock_open( + read_data='parameter_defaults: {ExistingKey: xyz}\n') + with mock.patch('six.moves.builtins.open', mock_open_context): + self.cmd._update_passwords_env( + passwords={'ADefault': 456, 'ExistingKey': 'dontupdate'}) + + mock_exists.assert_called_once_with(pw_env_path) + mock_open_context.assert_called_with(pw_env_path, 'w') + mock_open_handle = mock_open_context() + expected_dict = {'parameter_defaults': {'GeneratedPassword': 123, + 'ExistingKey': 'xyz', + 'ADefault': 456}} + mock_dump.assert_called_once_with(expected_dict, + mock_open_handle, + default_flow_style=False) diff --git a/tripleoclient/v1/undercloud_deploy.py b/tripleoclient/v1/undercloud_deploy.py index 93ef6eeeb..f5546317a 100644 --- a/tripleoclient/v1/undercloud_deploy.py +++ b/tripleoclient/v1/undercloud_deploy.py @@ -24,9 +24,18 @@ import subprocess import sys import tempfile import time -import urllib2 import yaml +try: + from urllib2 import HTTPError + from urllib2 import URLError + from urllib2 import urlopen +except ImportError: + # python3 + from urllib.error import HTTPError + from urllib.error import URLError + from urllib.request import urlopen + from cliff import command from heatclient.common import template_utils from openstackclient.i18n import _ @@ -76,12 +85,12 @@ class DeployUndercloud(command.Command): time.sleep(1) count += 1 try: - urllib2.urlopen("http://127.0.0.1:%s/" % api_port, timeout=1) - except urllib2.HTTPError as he: + urlopen("http://127.0.0.1:%s/" % api_port, timeout=1) + except HTTPError as he: if he.code == 300: return True pass - except urllib2.URLError: + except URLError: pass return False @@ -162,11 +171,17 @@ class DeployUndercloud(command.Command): os.kill(pid, signal.SIGKILL) if event_list_pid: os.kill(event_list_pid, signal.SIGKILL) - status = orchestration_client.stacks.get(stack_id).status - if status == 'FAILED': - return False + stack_get = orchestration_client.stacks.get(stack_id) + status = stack_get.status + if status != 'FAILED': + pw_rsrc = orchestration_client.resources.get( + stack_id, 'DefaultPasswords') + passwords = {p.title().replace("_", ""): v for p, v in + pw_rsrc.attributes.get('passwords', {}).items()} + return passwords else: - return True + msg = "Stack create failed, reason: %s" % stack_get.reason + raise Exception(msg) def _fork_heat_event_list(self): pid = os.fork() @@ -208,19 +223,26 @@ class DeployUndercloud(command.Command): else: return pid - def _generate_passwords_env(self): + def _update_passwords_env(self, passwords=None): pw_file = os.path.join(os.environ.get('HOME', ''), 'tripleo-undercloud-passwords.yaml') - stack_env = {} + stack_env = {'parameter_defaults': {}} if os.path.exists(pw_file): with open(pw_file) as pf: stack_env = yaml.load(pf.read()) pw = password_utils.generate_passwords(stack_env=stack_env) + stack_env['parameter_defaults'].update(pw) + + if passwords: + # These passwords are the DefaultPasswords so we only + # update if they don't already exist in stack_env + for p, v in passwords.items(): + if p not in stack_env['parameter_defaults']: + stack_env['parameter_defaults'][p] = v with open(pw_file, 'w') as pf: - yaml.safe_dump({'parameter_defaults': pw}, pf, - default_flow_style=False) + yaml.safe_dump(stack_env, pf, default_flow_style=False) return pw_file @@ -277,7 +299,7 @@ class DeployUndercloud(command.Command): environments.insert(0, resource_registry_path) # this will allow the user to overwrite passwords with custom envs - pw_file = self._generate_passwords_env() + pw_file = self._update_passwords_env() environments.insert(1, pw_file) undercloud_env_path = os.path.join( @@ -307,11 +329,17 @@ class DeployUndercloud(command.Command): environments.append(tmp_env_file.name) undercloud_yaml = os.path.join(tht_root, 'overcloud.yaml') - return self._heat_deploy(parsed_args.stack, undercloud_yaml, - parameters, environments, - parsed_args.timeout, - parsed_args.heat_api_port, - parsed_args.fake_keystone_port) + passwords = self._heat_deploy(parsed_args.stack, undercloud_yaml, + parameters, environments, + parsed_args.timeout, + parsed_args.heat_api_port, + parsed_args.fake_keystone_port) + if passwords: + # Get legacy passwords/secrets generated via heat + # These need to be written to the passwords file + # to avoid re-creating them every update + self._update_passwords_env(passwords) + return True def get_parser(self, prog_name): parser = argparse.ArgumentParser(