diff options
Diffstat (limited to 'tuskar_ui/infrastructure/overview')
-rw-r--r-- | tuskar_ui/infrastructure/overview/__init__.py | 0 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/overview/forms.py | 488 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/overview/panel.py | 26 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/overview/tests.py | 364 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/overview/urls.py | 38 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/overview/views.py | 390 |
6 files changed, 0 insertions, 1306 deletions
diff --git a/tuskar_ui/infrastructure/overview/__init__.py b/tuskar_ui/infrastructure/overview/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/tuskar_ui/infrastructure/overview/__init__.py +++ /dev/null diff --git a/tuskar_ui/infrastructure/overview/forms.py b/tuskar_ui/infrastructure/overview/forms.py deleted file mode 100644 index 77ff8c37..00000000 --- a/tuskar_ui/infrastructure/overview/forms.py +++ /dev/null @@ -1,488 +0,0 @@ -# -*- coding: utf8 -*- -# -# 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 logging -import six -import uuid - -from django.conf import settings -import django.forms -from django.utils.translation import ugettext_lazy as _ -import horizon.exceptions -import horizon.forms -import horizon.messages -from os_cloud_config import keystone as keystone_config -from os_cloud_config.utils import clients - -from tuskar_ui import api -import tuskar_ui.api.heat -import tuskar_ui.api.tuskar -import tuskar_ui.forms -import tuskar_ui.infrastructure.flavors.utils as flavors_utils -import tuskar_ui.utils.utils as tuskar_utils - -MATCHING_DEPLOYMENT_MODE = flavors_utils.matching_deployment_mode() -LOG = logging.getLogger(__name__) -MESSAGE_ICONS = { - 'ok': 'fa-check-square-o text-success', - 'pending': 'fa-square-o text-info', - 'error': 'fa-exclamation-circle text-danger', - 'warning': 'fa-exclamation-triangle text-warning', - None: 'fa-exclamation-triangle text-warning', -} -WEBROOT = getattr(settings, 'WEBROOT', '/') - - -def validate_roles(request, plan): - """Validates the roles in plan and returns dict describing the issues""" - for role in plan.role_list: - if ( - plan.get_role_node_count(role) and - not role.is_valid_for_deployment(plan) - ): - message = { - 'text': _(u"Configure Roles."), - 'is_critical': True, - 'status': 'pending', - } - break - else: - message = { - 'text': _(u"Configure Roles."), - 'status': 'ok', - } - return message - - -def validate_global_parameters(request, plan): - pending_required_global_params = list( - api.tuskar.Parameter.pending_parameters( - api.tuskar.Parameter.required_parameters( - api.tuskar.Parameter.global_parameters( - plan.parameter_list())))) - if pending_required_global_params: - message = { - 'text': _(u"Global Service Configuration."), - 'is_critical': True, - 'status': 'pending', - } - else: - message = { - 'text': _(u"Global Service Configuration."), - 'status': 'ok', - } - return message - - -def validate_plan(request, plan): - """Validates the plan and returns a list of dicts describing the issues.""" - messages = [] - requested_nodes = 0 - for role in plan.role_list: - node_count = plan.get_role_node_count(role) - requested_nodes += node_count - available_flavors = len(api.flavor.Flavor.list(request)) - if available_flavors == 0: - messages.append({ - 'text': _(u"Define Flavors."), - 'is_critical': True, - 'status': 'pending', - }) - else: - messages.append({ - 'text': _(u"Define Flavors."), - 'status': 'ok', - }) - available_nodes = len(api.node.Node.list(request, associated=False, - maintenance=False)) - if available_nodes == 0: - messages.append({ - 'text': _(u"Register Nodes."), - 'is_critical': True, - 'status': 'pending', - }) - elif requested_nodes > available_nodes: - messages.append({ - 'text': _(u"Not enough registered nodes for this plan. " - u"You need {0} more.").format( - requested_nodes - available_nodes), - 'is_critical': True, - 'status': 'error', - }) - else: - messages.append({ - 'text': _(u"Register Nodes."), - 'status': 'ok', - }) - messages.append(validate_roles(request, plan)) - messages.append(validate_global_parameters(request, plan)) - if not MATCHING_DEPLOYMENT_MODE: - # All roles have to have the same flavor. - default_flavor_name = api.flavor.Flavor.list(request)[0].name - for role in plan.role_list: - if role.flavor(plan).name != default_flavor_name: - messages.append({ - 'text': _(u"Role {0} doesn't use default flavor.").format( - role.name, - ), - 'is_critical': False, - 'statis': 'error', - }) - roles_assigned = True - messages.append({ - 'text': _(u"Assign roles."), - 'status': lambda: 'ok' if roles_assigned else 'pending', - }) - try: - controller_role = plan.get_role_by_name("Controller") - except KeyError: - messages.append({ - 'text': _(u"Controller Role Needed."), - 'is_critical': True, - 'status': 'error', - 'indent': 1, - }) - roles_assigned = False - else: - if plan.get_role_node_count(controller_role) not in (1, 3): - messages.append({ - 'text': _(u"1 or 3 Controllers Needed."), - 'is_critical': True, - 'status': 'pending', - 'indent': 1, - }) - roles_assigned = False - else: - messages.append({ - 'text': _(u"1 or 3 Controllers Needed."), - 'status': 'ok', - 'indent': 1, - }) - - try: - compute_role = plan.get_role_by_name("Compute") - except KeyError: - messages.append({ - 'text': _(u"Compute Role Needed."), - 'is_critical': True, - 'status': 'error', - 'indent': 1, - }) - roles_assigned = False - else: - if plan.get_role_node_count(compute_role) < 1: - messages.append({ - 'text': _(u"1 Compute Needed."), - 'is_critical': True, - 'status': 'pending', - 'indent': 1, - }) - roles_assigned = False - else: - messages.append({ - 'text': _(u"1 Compute Needed."), - 'status': 'ok', - 'indent': 1, - }) - for message in messages: - status = message.get('status') - if callable(status): - message['status'] = status = status() - message['classes'] = MESSAGE_ICONS.get(status, MESSAGE_ICONS[None]) - return messages - - -class EditPlan(horizon.forms.SelfHandlingForm): - def __init__(self, *args, **kwargs): - super(EditPlan, self).__init__(*args, **kwargs) - self.plan = api.tuskar.Plan.get_the_plan(self.request) - self.fields.update(self._role_count_fields(self.plan)) - - def _role_count_fields(self, plan): - fields = {} - for role in plan.role_list: - field = django.forms.IntegerField( - label=role.name, - widget=tuskar_ui.forms.NumberPickerInput(attrs={ - 'min': 1 if role.name in ('Controller', 'Compute') else 0, - 'step': 2 if role.name == 'Controller' else 1, - }), - initial=plan.get_role_node_count(role), - required=False - ) - field.role = role - fields['%s-count' % role.id] = field - return fields - - def handle(self, request, data): - parameters = dict( - (field.role.node_count_parameter_name, data[name]) - for (name, field) in self.fields.items() if name.endswith('-count') - ) - # NOTE(gfidente): this is a bad hack meant to magically add the - # parameter which enables Neutron L3 HA when the number of - # Controllers is > 1 - try: - controller_role = self.plan.get_role_by_name('Controller') - compute_role = self.plan.get_role_by_name('Compute') - except Exception as e: - LOG.warning('Unable to find a required role: %s', e.message) - else: - number_controllers = parameters[ - controller_role.node_count_parameter_name] - if number_controllers > 1: - for role in [controller_role, compute_role]: - l3ha_param = role.parameter_prefix + 'NeutronL3HA' - parameters[l3ha_param] = 'True' - l3agent_param = (role.parameter_prefix + - 'NeutronAllowL3AgentFailover') - parameters[l3agent_param] = 'True' - dhcp_agents_per_net = (number_controllers if number_controllers and - number_controllers > 3 else 3) - dhcp_agents_param = (controller_role.parameter_prefix + - 'NeutronDhcpAgentsPerNetwork') - parameters[dhcp_agents_param] = dhcp_agents_per_net - - try: - ceph_storage_role = self.plan.get_role_by_name('Ceph-Storage') - except Exception as e: - LOG.warning('Unable to find role: %s', 'Ceph-Storage') - else: - if parameters[ceph_storage_role.node_count_parameter_name] > 0: - parameters.update({ - 'CephClusterFSID': six.text_type(uuid.uuid4()), - 'CephMonKey': tuskar_utils.create_cephx_key(), - 'CephAdminKey': tuskar_utils.create_cephx_key() - }) - - cinder_enable_rbd_param = (controller_role.parameter_prefix - + 'CinderEnableRbdBackend') - glance_backend_param = (controller_role.parameter_prefix + - 'GlanceBackend') - nova_enable_rbd_param = (compute_role.parameter_prefix + - 'NovaEnableRbdBackend') - cinder_enable_iscsi_param = ( - controller_role.parameter_prefix + - 'CinderEnableIscsiBackend') - - parameters.update({ - cinder_enable_rbd_param: True, - glance_backend_param: 'rbd', - nova_enable_rbd_param: True, - cinder_enable_iscsi_param: False - }) - - try: - self.plan = self.plan.patch(request, self.plan.uuid, parameters) - except Exception as e: - horizon.exceptions.handle(request, _("Unable to update the plan.")) - LOG.exception(e) - return False - return True - - -class ScaleOut(EditPlan): - def __init__(self, *args, **kwargs): - super(ScaleOut, self).__init__(*args, **kwargs) - for name, field in self.fields.items(): - if name.endswith('-count'): - field.widget.attrs['min'] = field.initial - - def handle(self, request, data): - if not super(ScaleOut, self).handle(request, data): - return False - plan = self.plan - try: - stack = api.heat.Stack.get_by_plan(self.request, plan) - stack.update(request, plan.name, plan.templates) - except Exception as e: - LOG.exception(e) - if hasattr(e, 'error'): - horizon.exceptions.handle( - request, - _( - "Unable to deploy overcloud. Reason: {0}" - ).format(e.error['error']['message']), - ) - return False - else: - raise - else: - msg = _('Deployment in progress.') - horizon.messages.success(request, msg) - return True - - -class DeployOvercloud(horizon.forms.SelfHandlingForm): - network_isolation = horizon.forms.BooleanField( - label=_("Enable Network Isolation"), - required=False) - - def handle(self, request, data): - try: - plan = api.tuskar.Plan.get_the_plan(request) - except Exception as e: - LOG.exception(e) - horizon.exceptions.handle(request, - _("Unable to deploy overcloud.")) - return False - - # If network isolation selected, read environment file data - # and add to plan - env_temp = '/usr/share/openstack-tripleo-heat-templates/environments' - try: - if self.cleaned_data['network_isolation']: - with open(env_temp, 'r') as env_file: - env_contents = ''.join( - [line for line in - env_file.readlines() if '#' not in line] - ) - plan.environment += env_contents - except Exception as e: - LOG.exception(e) - pass - - # 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.get('is_critical'): - horizon.messages.success(request, message.text) - return False - try: - stack = api.heat.Stack.get_by_plan(self.request, plan) - if not stack: - api.heat.Stack.create(request, plan.name, plan.templates) - except Exception as e: - LOG.exception(e) - horizon.exceptions.handle( - request, _("Unable to deploy overcloud. Reason: {0}").format( - e.error['error']['message'])) - return False - else: - msg = _('Deployment in progress.') - horizon.messages.success(request, msg) - return True - - -class UndeployOvercloud(horizon.forms.SelfHandlingForm): - def handle(self, request, data): - try: - plan = api.tuskar.Plan.get_the_plan(request) - stack = api.heat.Stack.get_by_plan(self.request, plan) - if stack: - api.heat.Stack.delete(request, stack.id) - except Exception as e: - LOG.exception(e) - horizon.exceptions.handle(request, - _("Unable to undeploy overcloud.")) - return False - else: - msg = _('Undeployment in progress.') - horizon.messages.success(request, msg) - return True - - -class PostDeployInit(horizon.forms.SelfHandlingForm): - admin_email = horizon.forms.CharField(label=_("Admin Email")) - public_host = horizon.forms.CharField( - label=_("Public Host"), initial="", required=False) - region = horizon.forms.CharField( - label=_("Region"), initial="regionOne") - - def build_endpoints(self, plan, controller_role): - return { - "ceilometer": { - "password": plan.parameter_value( - controller_role.parameter_prefix + 'CeilometerPassword')}, - "cinder": { - "password": plan.parameter_value( - controller_role.parameter_prefix + 'CinderPassword')}, - "cinderv2": { - "password": plan.parameter_value( - controller_role.parameter_prefix + 'CinderPassword')}, - "ec2": { - "password": plan.parameter_value( - controller_role.parameter_prefix + 'GlancePassword')}, - "glance": { - "password": plan.parameter_value( - controller_role.parameter_prefix + 'GlancePassword')}, - "heat": { - "password": plan.parameter_value( - controller_role.parameter_prefix + 'HeatPassword')}, - "neutron": { - "password": plan.parameter_value( - controller_role.parameter_prefix + 'NeutronPassword')}, - "nova": { - "password": plan.parameter_value( - controller_role.parameter_prefix + 'NovaPassword')}, - "novav3": { - "password": plan.parameter_value( - controller_role.parameter_prefix + 'NovaPassword')}, - "swift": { - "password": plan.parameter_value( - controller_role.parameter_prefix + 'SwiftPassword'), - 'path': '/v1/AUTH_%(tenant_id)s', - 'admin_path': '/v1'}, - "horizon": { - 'port': '80', - 'path': WEBROOT, - 'admin_path': '%sadmin' % WEBROOT}} - - def handle(self, request, data): - try: - plan = api.tuskar.Plan.get_the_plan(request) - controller_role = plan.get_role_by_name("Controller") - stack = api.heat.Stack.get_by_plan(self.request, plan) - - admin_token = plan.parameter_value( - controller_role.parameter_prefix + 'AdminToken') - admin_password = plan.parameter_value( - controller_role.parameter_prefix + 'AdminPassword') - admin_email = data['admin_email'] - auth_ip = stack.keystone_ip - auth_url = stack.keystone_auth_url - auth_tenant = 'admin' - auth_user = 'admin' - - # do the keystone init - keystone_config.initialize( - auth_ip, admin_token, admin_email, admin_password, - region='regionOne', ssl=None, public=None, user='heat-admin', - pki_setup=False) - - # retrieve needed Overcloud clients - keystone_client = clients.get_keystone_client( - auth_user, admin_password, auth_tenant, auth_url) - - # do the setup endpoints - keystone_config.setup_endpoints( - self.build_endpoints(plan, controller_role), - public_host=data['public_host'], - region=data['region'], - os_auth_url=auth_url, - client=keystone_client) - - except Exception as e: - LOG.exception(e) - horizon.exceptions.handle(request, - _("Unable to initialize Overcloud.")) - return False - else: - msg = _('Overcloud has been initialized.') - horizon.messages.success(request, msg) - return True diff --git a/tuskar_ui/infrastructure/overview/panel.py b/tuskar_ui/infrastructure/overview/panel.py deleted file mode 100644 index 535cd13b..00000000 --- a/tuskar_ui/infrastructure/overview/panel.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf8 -*- -# -# 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. - -from django.utils.translation import ugettext_lazy as _ -import horizon - -from tuskar_ui.infrastructure import dashboard - - -class Overview(horizon.Panel): - name = _("Overview") - slug = "overview" - - -dashboard.Infrastructure.register(Overview) diff --git a/tuskar_ui/infrastructure/overview/tests.py b/tuskar_ui/infrastructure/overview/tests.py deleted file mode 100644 index 20155c86..00000000 --- a/tuskar_ui/infrastructure/overview/tests.py +++ /dev/null @@ -1,364 +0,0 @@ -# -*- coding: utf8 -*- -# -# 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 contextlib - -from django.core import urlresolvers -from mock import patch, call # noqa -from openstack_dashboard.test.test_data import utils - -from tuskar_ui import api -from tuskar_ui.infrastructure.overview import forms -from tuskar_ui.infrastructure.overview import views -from tuskar_ui.test import helpers as test -from tuskar_ui.test.test_data import heat_data -from tuskar_ui.test.test_data import tuskar_data - - -INDEX_URL = urlresolvers.reverse( - 'horizon:infrastructure:overview:index') -DEPLOY_URL = urlresolvers.reverse( - 'horizon:infrastructure:overview:deploy_confirmation') -DELETE_URL = urlresolvers.reverse( - 'horizon:infrastructure:overview:undeploy_confirmation') -POST_DEPLOY_INIT_URL = urlresolvers.reverse( - 'horizon:infrastructure:overview:post_deploy_init') -TEST_DATA = utils.TestDataContainer() -heat_data.data(TEST_DATA) -tuskar_data.data(TEST_DATA) - - -@contextlib.contextmanager -def _mock_plan(**kwargs): - plan = None - - params = { - 'spec_set': [ - 'create', - 'delete', - 'get', - 'get_the_plan', - 'id', - 'uuid', - 'patch', - 'parameters', - 'role_list', - 'parameter_value', - 'get_role_by_name', - 'get_role_node_count', - 'list_generated_parameters', - 'make_generated_parameters', - 'parameter_list', - ], - 'create.side_effect': lambda *args, **kwargs: plan, - 'delete.return_value': None, - 'get.side_effect': lambda *args, **kwargs: plan, - 'get_the_plan.side_effect': lambda *args, **kwargs: plan, - 'id': 'plan-1', - 'uuid': 'plan-1', - 'patch.side_effect': lambda *args, **kwargs: plan, - 'role_list': [], - 'parameter_list.return_value': [], - '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( - 'tuskar_ui.api.tuskar.Plan', **params) as Plan: - plan = Plan - yield Plan - - -class OverviewTests(test.BaseAdminViewTests): - def test_index_stack_not_created(self): - with contextlib.nested( - _mock_plan(), - patch('tuskar_ui.api.heat.Stack.list', return_value=[]), - patch('tuskar_ui.api.node.Node.list', return_value=[]), - patch('tuskar_ui.api.flavor.Flavor.list', return_value=[]), - ): - res = self.client.get(INDEX_URL) - get_the_plan = api.tuskar.Plan.get_the_plan - request = get_the_plan.call_args_list[0][0][0] - self.assertListEqual(get_the_plan.call_args_list, [ - call(request), - call(request), - call(request), - ]) - self.assertListEqual(api.heat.Stack.list.call_args_list, [ - call(request), - ]) - self.assertListEqual(api.node.Node.list.call_args_list, [ - call(request, associated=False, maintenance=False), - ]) - self.assertListEqual(api.flavor.Flavor.list.call_args_list, [ - call(request), - ]) - self.assertTemplateUsed( - res, 'infrastructure/overview/index.html') - self.assertTemplateUsed( - res, 'infrastructure/overview/role_nodes_edit.html') - - def test_index_stack_not_created_post(self): - with contextlib.nested( - _mock_plan(), - patch('tuskar_ui.api.heat.Stack.list', return_value=[]), - patch('tuskar_ui.api.node.Node.list', return_value=[]), - patch('tuskar_ui.api.flavor.Flavor.list', return_value=[]), - ) as (plan, _stack_list, _node_list, _flavor_list): - data = { - 'role-1-count': 1, - 'role-2-count': 0, - 'role-3-count': 0, - 'role-4-count': 0, - } - res = self.client.post(INDEX_URL, data) - self.assertNoFormErrors(res) - self.assertRedirectsNoFollow(res, INDEX_URL) - get_the_plan = api.tuskar.Plan.get_the_plan - request = get_the_plan.call_args_list[0][0][0] - self.assertListEqual(get_the_plan.call_args_list, [ - call(request), - ]) - self.assertListEqual( - api.tuskar.Plan.patch.call_args_list, - [call(request, plan.id, {})], - ) - - def test_index_stack_deployed(self): - stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first()) - roles = [api.tuskar.Role(role) - for role in self.tuskarclient_roles.list()] - - with contextlib.nested( - _mock_plan(**{'get_role_by_name.side_effect': None, - 'get_role_by_name.return_value': roles[0]}), - patch('tuskar_ui.api.heat.Stack.get_by_plan', - return_value=stack), - patch('tuskar_ui.api.heat.Stack.events', - return_value=[]), - ) as (Plan, stack_get_mock, stack_events_mock): - res = self.client.get(INDEX_URL) - request = Plan.get_the_plan.call_args_list[0][0][0] - self.assertListEqual( - Plan.get_the_plan.call_args_list, - [ - call(request), - call(request), - call(request), - ]) - - self.assertTemplateUsed( - res, 'infrastructure/overview/index.html') - self.assertTemplateUsed( - res, 'infrastructure/overview/deployment_live.html') - - def test_index_stack_undeploy_in_progress(self): - stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first()) - - with contextlib.nested( - _mock_plan(), - patch('tuskar_ui.api.heat.Stack.get_by_plan', - return_value=stack), - patch('tuskar_ui.api.heat.Stack.is_deleting', - return_value=True), - patch('tuskar_ui.api.heat.Stack.is_deployed', - return_value=False), - patch('tuskar_ui.api.heat.Stack.resources', - return_value=[]), - patch('tuskar_ui.api.heat.Stack.events', - return_value=[]), - ): - res = self.client.get(INDEX_URL) - - self.assertTemplateUsed( - res, 'infrastructure/overview/index.html') - self.assertTemplateUsed( - res, 'infrastructure/overview/deployment_progress.html') - - def test_deploy_get(self): - with _mock_plan(): - res = self.client.get(DEPLOY_URL) - self.assertTemplateUsed( - res, 'infrastructure/overview/deploy_confirmation.html') - - def test_delete_get(self): - stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first()) - - with contextlib.nested( - _mock_plan(), - patch('tuskar_ui.api.heat.Stack.get_by_plan', - return_value=stack), - ): - res = self.client.get(DELETE_URL) - self.assertTemplateUsed( - res, 'infrastructure/overview/undeploy_confirmation.html') - - def test_delete_post(self): - stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first()) - - with contextlib.nested( - _mock_plan(), - patch('tuskar_ui.api.heat.Stack.get_by_plan', - return_value=stack), - patch('tuskar_ui.api.heat.Stack.delete', - return_value=None), - ): - res = self.client.post(DELETE_URL) - self.assertRedirectsNoFollow(res, INDEX_URL) - - def test_post_deploy_init_get(self): - stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first()) - - with contextlib.nested( - _mock_plan(), - patch('tuskar_ui.api.heat.Stack.get_by_plan', - return_value=stack), - ): - res = self.client.get(POST_DEPLOY_INIT_URL) - self.assertEqual(res.context['form']['admin_email'].value(), '') - self.assertTemplateUsed( - res, 'infrastructure/overview/post_deploy_init.html') - - def test_post_deploy_init_post(self): - stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first()) - roles = [api.tuskar.Role(role) - for role in self.tuskarclient_roles.list()] - - data = { - 'admin_email': "example@example.org", - 'public_host': '', - 'region': 'regionOne', - } - - with contextlib.nested( - _mock_plan(**{'get_role_by_name.side_effect': None, - 'get_role_by_name.return_value': roles[0]}), - patch('tuskar_ui.api.heat.Stack.get_by_plan', - return_value=stack), - patch('os_cloud_config.keystone.initialize', - return_value=None), - patch('os_cloud_config.keystone.setup_endpoints', - return_value=None), - patch('os_cloud_config.utils.clients.get_keystone_client', - return_value='keystone_client'), - ) as (mock_plan, mock_get_by_plan, mock_initialize, - mock_setup_endpoints, mock_get_keystone_client): - res = self.client.post(POST_DEPLOY_INIT_URL, data) - - self.assertNoFormErrors(res) - self.assertEqual(res.status_code, 302) - self.assertRedirectsNoFollow(res, INDEX_URL) - - mock_initialize.assert_called_once_with( - '192.0.2.23', None, 'example@example.org', None, ssl=None, - region='regionOne', user='heat-admin', public=None, - pki_setup=False) - mock_setup_endpoints.assert_called_once_with( - {'nova': {'password': None}, - 'heat': {'password': None}, - 'ceilometer': {'password': None}, - 'ec2': {'password': None}, - "horizon": { - 'port': '80', - 'path': '/', - 'admin_path': '/admin'}, - 'cinder': {'password': None}, - 'cinderv2': {'password': None}, - 'glance': {'password': None}, - 'swift': {'password': None, - 'path': '/v1/AUTH_%(tenant_id)s', - 'admin_path': '/v1'}, - 'novav3': {'password': None}, - 'neutron': {'password': None}}, - os_auth_url=stack.keystone_auth_url, - client='keystone_client', - region='regionOne', - public_host='') - mock_get_keystone_client.assert_called_once_with( - 'admin', None, 'admin', stack.keystone_auth_url) - - def test_get_role_data(self): - plan = api.tuskar.Plan(self.tuskarclient_plans.first()) - stack = api.heat.Stack(self.heatclient_stacks.first()) - role = api.tuskar.Role(self.tuskarclient_roles.first()) - stack.resources = lambda *args, **kwargs: [] - ret = views._get_role_data(plan, stack, None, role) - self.assertEqual(ret, { - 'deployed_node_count': 0, - 'deploying_node_count': 0, - 'error_node_count': 0, - 'field': '', - 'finished': False, - 'icon': 'fa-exclamation', - 'id': 'role-1', - 'name': 'Controller', - 'planned_node_count': 1, - 'role': role, - 'status': 'warning', - 'total_node_count': 0, - 'waiting_node_count': 0, - }) - - def test_validate_plan_empty(self): - with ( - _mock_plan() - ) as plan, ( - patch('tuskar_ui.api.node.Node.list', return_value=[]) - ), ( - patch('tuskar_ui.api.flavor.Flavor.list', return_value=[]) - ): - ret = forms.validate_plan(None, plan) - for m in ret: - m['text'] = unicode(m['text']) - self.assertEqual(ret, [ - { - 'is_critical': True, - 'text': u'Define Flavors.', - 'status': 'pending', - 'classes': 'fa-square-o text-info', - }, { - 'is_critical': True, - 'text': u'Register Nodes.', - 'status': 'pending', - 'classes': 'fa-square-o text-info', - }, { - 'status': 'ok', - 'text': u'Configure Roles.', - 'classes': 'fa-check-square-o text-success', - }, { - 'status': 'ok', - 'text': u'Global Service Configuration.', - 'classes': 'fa-check-square-o text-success', - }, { - 'status': 'pending', - 'text': u'Assign roles.', - 'classes': 'fa-square-o text-info', - }, { - 'is_critical': True, - 'text': u'Controller Role Needed.', - 'status': 'error', - 'indent': 1, - 'classes': 'fa-exclamation-circle text-danger', - }, { - 'is_critical': True, - 'text': u'Compute Role Needed.', - 'status': 'error', - 'indent': 1, - 'classes': 'fa-exclamation-circle text-danger', - }, - ]) diff --git a/tuskar_ui/infrastructure/overview/urls.py b/tuskar_ui/infrastructure/overview/urls.py deleted file mode 100644 index 35410d5d..00000000 --- a/tuskar_ui/infrastructure/overview/urls.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf8 -*- -# -# 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. - -from django.conf import urls - -from tuskar_ui.infrastructure.overview import views - - -urlpatterns = urls.patterns( - '', - urls.url(r'^$', views.IndexView.as_view(), name='index'), - urls.url(r'^deploy-confirmation$', - views.DeployConfirmationView.as_view(), - name='deploy_confirmation'), - urls.url(r'^undeploy-confirmation$', - views.UndeployConfirmationView.as_view(), - name='undeploy_confirmation'), - urls.url(r'^post-deploy-init$', - views.PostDeployInitView.as_view(), - name='post_deploy_init'), - urls.url(r'^scale-out$', - views.ScaleOutView.as_view(), - name='scale_out'), - urls.url(r'^download-overcloudrc$', - views.download_overcloudrc_file, - name='download_overcloudrc'), -) diff --git a/tuskar_ui/infrastructure/overview/views.py b/tuskar_ui/infrastructure/overview/views.py deleted file mode 100644 index 90f9fa37..00000000 --- a/tuskar_ui/infrastructure/overview/views.py +++ /dev/null @@ -1,390 +0,0 @@ -# -*- coding: utf8 -*- -# -# 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 json -import logging -import urlparse - -from django.core.urlresolvers import reverse -from django.core.urlresolvers import reverse_lazy -from django import http -from django import shortcuts -import django.utils.text -from django.utils.translation import ugettext_lazy as _ -import heatclient -import horizon.forms -from horizon import messages - -from tuskar_ui import api -from tuskar_ui.infrastructure.overview import forms -from tuskar_ui.infrastructure import views - - -INDEX_URL = 'horizon:infrastructure:overview:index' - -LOG = logging.getLogger(__name__) - - -def _steps_message(messages): - total_steps = len(messages) - completed_steps = len([m for m in messages if not m.get('is_critical')]) - return _("{0} of {1} Steps Completed").format(completed_steps, total_steps) - - -def _get_role_data(plan, stack, form, role): - """Gathers data about a single deployment role. - - Gathers data about a single deployment role from the related Overcloud - and Role objects, and presents it in the form convenient for use - from the template. - - """ - data = { - 'id': role.id, - 'role': role, - 'name': role.name, - 'planned_node_count': plan.get_role_node_count(role), - 'field': form['%s-count' % role.id] if form else '', - } - - if stack: - resources = stack.resources(role=role, with_joins=True) - nodes = [r.node for r in resources] - node_count = len(nodes) - - deployed_node_count = 0 - deploying_node_count = 0 - error_node_count = 0 - waiting_node_count = node_count - - status = 'warning' - if nodes: - deployed_node_count = sum(1 for node in nodes - if node.instance.status == 'ACTIVE') - deploying_node_count = sum(1 for node in nodes - if node.instance.status == 'BUILD') - error_node_count = sum(1 for node in nodes - if node.instance.status == 'ERROR') - waiting_node_count = (node_count - deployed_node_count - - deploying_node_count - error_node_count) - - if error_node_count or 'FAILED' in stack.stack_status: - status = 'danger' - elif deployed_node_count == data['planned_node_count']: - status = 'success' - else: - status = 'info' - - finished = deployed_node_count == data['planned_node_count'] - if finished: - icon = 'fa-check' - elif status in ('danger', 'warning'): - icon = 'fa-exclamation' - else: - icon = 'fa-spinner fa-spin' - - data.update({ - 'status': status, - 'finished': finished, - 'total_node_count': node_count, - 'deployed_node_count': deployed_node_count, - 'deploying_node_count': deploying_node_count, - 'waiting_node_count': waiting_node_count, - 'error_node_count': error_node_count, - 'icon': icon, - }) - - # TODO(rdopieralski) get this from ceilometer - # data['capacity'] = 20 - return data - - -class IndexView(horizon.forms.ModalFormView, views.StackMixin): - template_name = 'infrastructure/overview/index.html' - form_class = forms.EditPlan - success_url = reverse_lazy(INDEX_URL) - - def get_progress_update(self, request, data): - return { - 'progress': data.get('progress'), - 'show_last_events': data.get('show_last_events'), - 'last_events_title': unicode(data.get('last_events_title')), - 'last_events': [{ - 'event_time': event.event_time, - 'resource_name': event.resource_name, - 'resource_status': event.resource_status, - 'resource_status_reason': event.resource_status_reason, - } for event in data.get('last_events', [])], - 'roles': [{ - 'status': role.get('status', 'warning'), - 'finished': role.get('finished', False), - 'name': role.get('name', ''), - 'slug': django.utils.text.slugify(role.get('name', '')), - 'id': role.get('id', ''), - 'total_node_count': role.get('node_count', 0), - 'deployed_node_count': role.get('deployed_node_count', 0), - 'deploying_node_count': role.get('deploying_node_count', 0), - 'waiting_node_count': role.get('waiting_node_count', 0), - 'error_node_count': role.get('error_node_count', 0), - 'planned_node_count': role.get('planned_node_count', 0), - 'icon': role.get('icon', ''), - } for role in data.get('roles', [])], - } - - def get(self, request, *args, **kwargs): - if request.META.get('HTTP_X_HORIZON_PROGRESS', ''): - # If it's an AJAX call for progress update, send it. - data = self.get_data(request, {}) - return http.HttpResponse( - json.dumps(self.get_progress_update(request, data)), - content_type='application/json', - ) - return super(IndexView, self).get(request, *args, **kwargs) - - def get_form(self, form_class): - return form_class(self.request, **self.get_form_kwargs()) - - def get_context_data(self, *args, **kwargs): - context = super(IndexView, self).get_context_data(*args, **kwargs) - context.update(self.get_data(self.request, context)) - return context - - def get_data(self, request, context, *args, **kwargs): - plan = api.tuskar.Plan.get_the_plan(request) - stack = self.get_stack() - form = context.get('form') - - context['plan'] = plan - context['stack'] = stack - - roles = [_get_role_data(plan, stack, form, role) - for role in plan.role_list] - context['roles'] = roles - - if stack: - context['show_last_events'] = True - failed_events = [e for e in stack.events - if 'FAILED' in e.resource_status and - 'aborted' not in e.resource_status_reason][-3:] - - if failed_events: - context['last_events_title'] = _('Last failed events') - context['last_events'] = failed_events - else: - context['last_events_title'] = _('Last event') - context['last_events'] = [stack.events[0]] - - if stack.is_deleting or stack.is_delete_failed: - # TODO(lsmola) since at this point we don't have total number - # of nodes we will hack this around, till API can show this - # information. So it will actually show progress like the total - # number is 10, or it will show progress of 5%. Ugly, but - # workable. - total_num_nodes_count = 10 - - try: - resources_count = len( - stack.resources(with_joins=False)) - except heatclient.exc.HTTPNotFound: - # Immediately after undeploying has started, heat returns - # this exception so we can take it as kind of init of - # undeploying. - resources_count = total_num_nodes_count - - # TODO(lsmola) same as hack above - total_num_nodes_count = max( - resources_count, total_num_nodes_count) - - context['progress'] = min(95, max( - 5, 100 * float(resources_count) / total_num_nodes_count)) - elif stack.is_deploying or stack.is_updating: - total = sum(d['total_node_count'] for d in roles) - context['progress'] = min(95, max( - 5, 100 * sum(float(d.get('deployed_node_count', 0)) - for d in roles) / (total or 1) - )) - else: - # stack is active - if not stack.is_failed: - context['show_last_events'] = False - context['progress'] = 100 - controller_role = plan.get_role_by_name("Controller") - context['admin_password'] = plan.parameter_value( - controller_role.parameter_prefix + 'AdminPassword') - - context['dashboard_urls'] = stack.dashboard_urls - no_proxy = [urlparse.urlparse(url).hostname - for url in stack.dashboard_urls] - context['no_proxy'] = ",".join(no_proxy) - context['auth_url'] = stack.keystone_auth_url - else: - messages = forms.validate_plan(request, plan) - context['plan_messages'] = messages - context['plan_invalid'] = any(message.get('is_critical') - for message in messages) - context['steps_message'] = _steps_message(messages) - return context - - def post(self, request, *args, **kwargs): - """If the post comes from ajax, return validation results as json.""" - - if not request.META.get('HTTP_X_HORIZON_VALIDATE', ''): - return super(IndexView, self).post(request, *args, **kwargs) - form_class = self.get_form_class() - form = self.get_form(form_class) - if form.is_valid(): - handled = form.handle(self.request, form.cleaned_data) - else: - handled = False - if handled: - messages = forms.validate_plan(request, form.plan) - else: - messages = [{ - 'text': _(u"Error saving the plan."), - 'is_critical': True, - }] - messages.extend({ - 'text': repr(error), - } for error in form.non_field_errors) - messages.extend({ - 'text': repr(error), - } for field in form.fields for error in field.errors) - # We need to unlazify all the lazy urls and translations. - return http.HttpResponse(json.dumps({ - 'plan_invalid': any(m.get('is_critical') for m in messages), - 'steps_message': _steps_message(messages), - 'messages': [{ - 'text': unicode(m.get('text', '')), - 'is_critical': m.get('is_critical', False), - 'indent': m.get('indent', 0), - 'classes': m['classes'], - } for m in messages], - }), content_type='application/json') - - -class DeployConfirmationView(horizon.forms.ModalFormView, views.StackMixin): - form_class = forms.DeployOvercloud - template_name = 'infrastructure/overview/deploy_confirmation.html' - submit_label = _("Deploy") - - 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) - - -class UndeployConfirmationView(horizon.forms.ModalFormView, views.StackMixin): - form_class = forms.UndeployOvercloud - template_name = 'infrastructure/overview/undeploy_confirmation.html' - submit_label = _("Undeploy") - - def get_success_url(self): - return reverse(INDEX_URL) - - def get_context_data(self, **kwargs): - context = super(UndeployConfirmationView, - self).get_context_data(**kwargs) - context['stack_id'] = self.get_stack().id - return context - - def get_initial(self, **kwargs): - initial = super(UndeployConfirmationView, self).get_initial(**kwargs) - initial['stack_id'] = self.get_stack().id - return initial - - -class PostDeployInitView(horizon.forms.ModalFormView, views.StackMixin): - form_class = forms.PostDeployInit - template_name = 'infrastructure/overview/post_deploy_init.html' - submit_label = _("Initialize") - - def get_success_url(self): - return reverse(INDEX_URL) - - def get_context_data(self, **kwargs): - context = super(PostDeployInitView, - self).get_context_data(**kwargs) - context['stack_id'] = self.get_stack().id - return context - - def get_initial(self, **kwargs): - initial = super(PostDeployInitView, self).get_initial(**kwargs) - initial['stack_id'] = self.get_stack().id - initial['admin_email'] = getattr(self.request.user, 'email', '') - return initial - - -class ScaleOutView(horizon.forms.ModalFormView, views.StackMixin): - form_class = forms.ScaleOut - template_name = "infrastructure/overview/scale_out.html" - submit_label = _("Deploy Changes") - - def get_success_url(self): - return reverse(INDEX_URL) - - def get_form(self, form_class): - return form_class(self.request, **self.get_form_kwargs()) - - def get_context_data(self, *args, **kwargs): - context = super(ScaleOutView, self).get_context_data(*args, **kwargs) - plan = api.tuskar.Plan.get_the_plan(self.request) - form = context.get('form') - roles = [_get_role_data(plan, None, form, role) - for role in plan.role_list] - context.update({ - 'roles': roles, - 'plan': plan, - }) - return context - - -def _get_openrc_credentials(request): - plan = api.tuskar.Plan.get_the_plan(request) - stack = api.heat.Stack.get_by_plan(request, plan) - no_proxy = [urlparse.urlparse(url).hostname - for url in stack.dashboard_urls] - controller_role = plan.get_role_by_name("Controller") - credentials = dict(tenant_name='admin', - auth_url=stack.keystone_auth_url, - admin_password=plan.parameter_value( - controller_role.parameter_prefix + 'AdminPassword'), - no_proxy=",".join(no_proxy)) - return credentials - - -def download_overcloudrc_file(request): - template = 'infrastructure/overview/overcloudrc.sh.template' - try: - context = _get_openrc_credentials(request) - - response = shortcuts.render(request, - template, - context, - content_type="text/plain") - response['Content-Disposition'] = ('attachment; ' - 'filename="overcloudrc"') - response['Content-Length'] = str(len(response.content)) - return response - - except Exception as e: - LOG.exception("Exception in DownloadOvercloudrcForm.") - messages.error(request, _('Error Downloading RC File: %s') % e) - return shortcuts.redirect(request.build_absolute_uri()) |