From 5b00461714d06ce7ce9bf5d539dc5c61db9f5f66 Mon Sep 17 00:00:00 2001 From: Ladislav Smola Date: Tue, 23 Sep 2014 17:35:02 +0200 Subject: [PATCH] Autogenerate missing parameters We will generate certain parameters if they are missing from user input. This includes passwords, keystone certificates and neutron control plane id. Change-Id: I70b8bce1f0b2df7d2952b2c4b4761cb59970c1f2 --- tuskar_ui/api/heat.py | 1 - tuskar_ui/api/tuskar.py | 118 ++++++++++++++++++ tuskar_ui/infrastructure/overview/forms.py | 14 ++- .../overview/_deploy_confirmation.html | 4 + tuskar_ui/infrastructure/overview/tests.py | 4 + tuskar_ui/infrastructure/overview/views.py | 9 ++ 6 files changed, 146 insertions(+), 4 deletions(-) diff --git a/tuskar_ui/api/heat.py b/tuskar_ui/api/heat.py index 12ac83ada..69ef7e589 100644 --- a/tuskar_ui/api/heat.py +++ b/tuskar_ui/api/heat.py @@ -80,7 +80,6 @@ class Stack(base.APIResourceWrapper): self._request = request @classmethod - @handle_errors(_("Unable to create Heat stack"), []) def create(cls, request, stack_name, template, environment, provider_resource_templates): fields = { diff --git a/tuskar_ui/api/tuskar.py b/tuskar_ui/api/tuskar.py index 8e0040209..c3e1c5a38 100644 --- a/tuskar_ui/api/tuskar.py +++ b/tuskar_ui/api/tuskar.py @@ -11,11 +11,15 @@ # under the License. import logging +import random +import string from django.conf import settings from django.utils.translation import ugettext_lazy as _ from openstack_dashboard.api import base from openstack_dashboard.api import glance +from openstack_dashboard.api import neutron +from os_cloud_config import keystone_pki from tuskarclient import client as tuskar_client from tuskar_ui.api import flavor @@ -27,6 +31,11 @@ MASTER_TEMPLATE_NAME = 'plan.yaml' ENVIRONMENT_NAME = 'environment.yaml' TUSKAR_SERVICE = 'management' +SSL_HIDDEN_PARAMS = ('SSLCertificate', 'SSLKey') +KEYSTONE_CERTIFICATE_PARAMS = ( + 'KeystoneSigningCertificate', 'KeystoneCACertificate', + 'KeystoneSigningKey') + # FIXME: request isn't used right in the tuskar client right now, # but looking at other clients, it seems like it will be in the future @@ -49,6 +58,48 @@ def tuskarclient(request, password=None): return client +def password_generator(size=40, chars=(string.ascii_uppercase + + string.ascii_lowercase + + string.digits)): + return ''.join(random.choice(chars) for _ in range(size)) + + +def strip_prefix(parameter_name): + return parameter_name.split('::', 1)[-1] + + +def _is_blank(parameter): + return not parameter['value'] or parameter['value'] == 'unset' + + +def _should_generate_password(parameter): + # TODO(lsmola) Filter out SSL params for now. Once it will be generated + # in TripleO add it here too. Note: this will also affect how endpoints are + # created + key = parameter['name'] + return all([ + parameter['hidden'], + _is_blank(parameter), + strip_prefix(key) not in SSL_HIDDEN_PARAMS, + strip_prefix(key) not in KEYSTONE_CERTIFICATE_PARAMS, + key != 'SnmpdReadonlyUserPassword', + ]) + + +def _should_generate_keystone_cert(parameter): + return all([ + strip_prefix(parameter['name']) in KEYSTONE_CERTIFICATE_PARAMS, + _is_blank(parameter), + ]) + + +def _should_generate_neutron_control_plane(parameter): + return all([ + strip_prefix(parameter['name']) == 'NeutronControlPlaneID', + _is_blank(parameter), + ]) + + class Plan(base.APIResourceWrapper): _attrs = ('uuid', 'name', 'description', 'created_at', 'modified_at', 'roles', 'parameters') @@ -219,6 +270,73 @@ class Plan(base.APIResourceWrapper): return parameter['value'] return default + def list_generated_parameters(self, with_prefix=True): + if with_prefix: + key_format = lambda key: key + else: + key_format = strip_prefix + + # Get all password like parameters + return dict( + (key_format(parameter['name']), parameter) + for parameter in self.parameter_list() + if any([ + _should_generate_password(parameter), + _should_generate_keystone_cert(parameter), + _should_generate_neutron_control_plane(parameter), + ]) + ) + + def _make_keystone_certificates(self, wanted_generated_params): + generated_params = {} + for cert_param in KEYSTONE_CERTIFICATE_PARAMS: + if cert_param in wanted_generated_params.keys(): + # If one of the keystone certificates is not set, we have + # to generate all of them. + generate_certificates = True + break + else: + generate_certificates = False + + # Generate keystone certificates + if generate_certificates: + ca_key_pem, ca_cert_pem = keystone_pki.create_ca_pair() + signing_key_pem, signing_cert_pem = ( + keystone_pki.create_signing_pair(ca_key_pem, ca_cert_pem)) + generated_params['KeystoneSigningCertificate'] = ( + signing_cert_pem) + generated_params['KeystoneCACertificate'] = ca_cert_pem + generated_params['KeystoneSigningKey'] = signing_key_pem + return generated_params + + def make_generated_parameters(self): + wanted_generated_params = self.list_generated_parameters( + with_prefix=False) + + # Generate keystone certificates + generated_params = self._make_keystone_certificates( + wanted_generated_params) + + # Generate passwords and control plane id + for (key, param) in wanted_generated_params.items(): + if _should_generate_password(param): + generated_params[key] = password_generator() + elif _should_generate_neutron_control_plane(param): + generated_params[key] = neutron.network_list( + self._request, name='ctlplane')[0].id + + # Fill all the Tuskar parameters with generated content. There are + # parameters that has just different prefix, such parameters should + # have the same values. + wanted_prefixed_params = self.list_generated_parameters( + with_prefix=True) + tuskar_params = {} + + for (key, param) in wanted_prefixed_params.items(): + tuskar_params[key] = generated_params[strip_prefix(key)] + + return tuskar_params + @property def id(self): return self.uuid diff --git a/tuskar_ui/infrastructure/overview/forms.py b/tuskar_ui/infrastructure/overview/forms.py index e45bf3d3d..dab156726 100644 --- a/tuskar_ui/infrastructure/overview/forms.py +++ b/tuskar_ui/infrastructure/overview/forms.py @@ -79,7 +79,7 @@ def validate_plan(request, plan): }) requested_nodes += plan.get_role_node_count(role) snmp_password = plan.parameter_value( - role.paremeter_prefix + 'SnmpdReadonlyUserPassword') + role.parameter_prefix + 'SnmpdReadonlyUserPassword') if (not snmp_password or previous_snmp_password and previous_snmp_password != snmp_password): @@ -165,6 +165,13 @@ class DeployOvercloud(horizon.forms.SelfHandlingForm): horizon.exceptions.handle(request, _("Unable to deploy overcloud.")) return False + + # Auto-generate missing passwords and certificates + if plan.list_generated_parameters(): + generated_params = plan.make_generated_parameters() + plan = plan.patch(request, plan.uuid, generated_params) + + # Validate plan and create stack for message in validate_plan(request, plan): if message['is_critical']: horizon.messages.success(request, message.text) @@ -179,8 +186,9 @@ class DeployOvercloud(horizon.forms.SelfHandlingForm): plan.provider_resource_templates) except Exception as e: LOG.exception(e) - horizon.exceptions.handle(request, - _("Unable to deploy overcloud.")) + horizon.exceptions.handle( + request, _("Unable to deploy overcloud. Reason: {0}").format( + e.error['error']['message'])) return False else: msg = _('Deployment in progress.') diff --git a/tuskar_ui/infrastructure/overview/templates/overview/_deploy_confirmation.html b/tuskar_ui/infrastructure/overview/templates/overview/_deploy_confirmation.html index ce069dc5c..c8b5e8403 100644 --- a/tuskar_ui/infrastructure/overview/templates/overview/_deploy_confirmation.html +++ b/tuskar_ui/infrastructure/overview/templates/overview/_deploy_confirmation.html @@ -12,6 +12,10 @@

{% trans "You are about deploy your overcloud" %}

+ {% if autogenerated_parameters %} + These parameters will be randomly generated before the deployment: +

{{ autogenerated_parameters|join:", " }}

+ {% endif %}

{% trans "This operation cannot be undone. Are you sure you want to do that?" %}

{% endblock %} diff --git a/tuskar_ui/infrastructure/overview/tests.py b/tuskar_ui/infrastructure/overview/tests.py index d1dd749d5..c098d2892 100644 --- a/tuskar_ui/infrastructure/overview/tests.py +++ b/tuskar_ui/infrastructure/overview/tests.py @@ -55,6 +55,8 @@ def _mock_plan(**kwargs): 'parameter_value', 'get_role_by_name', 'get_role_node_count', + 'list_generated_parameters', + 'make_generated_parameters', ], 'create.side_effect': lambda *args, **kwargs: plan, 'delete.return_value': None, @@ -67,6 +69,8 @@ def _mock_plan(**kwargs): 'parameter_value.return_value': None, 'get_role_by_name.side_effect': KeyError, 'get_role_node_count.return_value': 0, + 'list_generated_parameters.return_value': {}, + 'make_generated_parameters.return_value': {}, } params.update(kwargs) with patch( diff --git a/tuskar_ui/infrastructure/overview/views.py b/tuskar_ui/infrastructure/overview/views.py index 8569c0d1d..bfd4f1100 100644 --- a/tuskar_ui/infrastructure/overview/views.py +++ b/tuskar_ui/infrastructure/overview/views.py @@ -156,6 +156,15 @@ class DeployConfirmationView(horizon.forms.ModalFormView, StackMixin): form_class = forms.DeployOvercloud template_name = 'infrastructure/overview/deploy_confirmation.html' + def get_context_data(self, **kwargs): + context = super(DeployConfirmationView, + self).get_context_data(**kwargs) + plan = api.tuskar.Plan.get_the_plan(self.request) + + context['autogenerated_parameters'] = ( + plan.list_generated_parameters(with_prefix=False).keys()) + return context + def get_success_url(self): return reverse(INDEX_URL)