Merge "Drop Heat related code from horizon"

This commit is contained in:
Zuul 2017-12-09 10:27:43 +00:00 committed by Gerrit Code Review
commit b4fccffccc
94 changed files with 17 additions and 5977 deletions

View File

@ -30,8 +30,3 @@ As an administrative user, you can view information for OpenStack services.
* :guilabel:`Network Agents`:
Displays the network agents active within the cluster, such as L3 and
DHCP agents, and the status of each agent.
* :guilabel:`Orchestration Services`:
Displays information specific to the Orchestration service. Name,
engine id, host and topic are listed for each service, as well as its
activation status.

View File

@ -116,7 +116,6 @@ You can also override existing methods with your own versions::
NO = lambda *x: False
tabs.HeatServiceTab.allowed = NO
tables.AssociateIP.allowed = NO
tables.SimpleAssociateIP.allowed = NO
tables.SimpleDisassociateIP.allowed = NO

View File

@ -804,7 +804,6 @@ Default:
'compute': 'nova_policy.json',
'volume': 'cinder_policy.json',
'image': 'glance_policy.json',
'orchestration': 'heat_policy.json',
'network': 'neutron_policy.json',
}
@ -1125,29 +1124,6 @@ Default:
Used to customize features related to the image service, such as the list of
supported image formats.
Heat
----
OPENSTACK_HEAT_STACK
~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 9.0.0(Mitaka)
Default:
.. code-block:: python
{
'enable_user_pass': True
}
A dictionary of settings to use with heat stacks. Currently, the only setting
available is "enable_user_pass", which can be used to disable the password
field while launching the stack. Currently HEAT API needs user password to
perform all the heat operations because in HEAT API trusts is not enabled by
default. So, this setting can be set as "False" in-case HEAT uses trusts by
default otherwise it needs to be set as "True".
Keystone
--------

View File

@ -83,7 +83,7 @@ To start the Horizon development server use the command below
.. note::
The default port for runserver is 8000 which is already consumed by
The default port for runserver is 8000 which might be already consumed by
heat-api-cfn in DevStack. If running in DevStack
``tox -e runserver -- localhost:9000`` will start the test server at
``http://localhost:9000``

View File

@ -69,9 +69,6 @@ see https://docs.openstack.org/devstack/latest/
SWIFT_REPLICAS=1
SWIFT_DATA_DIR=$DEST/data/swift
# Enable Heat
enable_plugin heat https://git.openstack.org/openstack/heat
# Enable Neutron
enable_plugin neutron https://git.openstack.org/openstack/neutron

View File

@ -22,7 +22,6 @@ System Requirements
* `cinder <https://docs.openstack.org/cinder/latest/>`_: Block Storage
* `glance <https://docs.openstack.org/glance/latest/>`_: Image Management
* `heat <https://docs.openstack.org/heat/latest/>`_: Orchestration
* `neutron <https://docs.openstack.org/neutron/latest/>`_: Networking
* `nova <https://docs.openstack.org/nova/latest/>`_: Compute
* `swift <https://docs.openstack.org/swift/latest/>`_: Object Storage

View File

@ -18,7 +18,6 @@ sizes of server instances.
manage-containers.rst
manage-volumes.rst
manage-shares.rst
stacks.rst
databases.rst
manage-lbaasv2.rst
browser_support

View File

@ -50,11 +50,6 @@ The dashboard is generally installed on the controller node.
(:ref:`dashboard-admin-tab`) and :guilabel:`Identity` tab
(:ref:`dashboard-identity-tab`) are displayed.
.. note::
Some tabs, such as :guilabel:`Orchestration` and :guilabel:`Firewalls`,
only appear on the dashboard if they are properly configured.
.. _dashboard-project-tab:
OpenStack dashboard — Project tab
@ -143,15 +138,6 @@ Network tab
* :guilabel:`Firewall Rules`: Add and manage firewall rules.
Orchestration tab
-----------------
* :guilabel:`Stacks`: Use the REST API to orchestrate multiple composite
cloud applications.
* :guilabel:`Resource Types`: Show a list of all the supported resource
types for HOT templates.
Object Store tab
----------------
@ -234,9 +220,6 @@ System tab
* :guilabel:`Network Agents`: View the network agents.
* :guilabel:`Orchestration Services`: View a list of all Orchestration
services.
* :guilabel:`Shares`: Use the following tabs to complete these tasks:
* :guilabel:`Shares`: View, create, manage, and delete shares.

View File

@ -1,149 +0,0 @@
========================
Launch and manage stacks
========================
OpenStack Orchestration is a service that you can use to
orchestrate multiple composite cloud applications. This
service supports the use of both the Amazon Web Services (AWS)
CloudFormation template format through both a Query API that
is compatible with CloudFormation and the native OpenStack
Heat Orchestration Template (HOT) format through a REST API.
These flexible template languages enable application
developers to describe and automate the deployment of
infrastructure, services, and applications. The templates
enable creation of most OpenStack resource types, such as
instances, floating IP addresses, volumes, security groups,
and users. Once created, the resources are referred to as
stacks.
The template languages are described in the `Template Guide
<https://docs.openstack.org/heat/latest/template_guide/>`_.
Launch a stack
~~~~~~~~~~~~~~
#. Log in to the dashboard.
#. Select the appropriate project from the drop down menu at the top left.
#. On the :guilabel:`Project` tab, open the :guilabel:`Orchestration` tab and
click :guilabel:`Stacks` category.
#. Click :guilabel:`Launch Stack`.
#. In the :guilabel:`Select Template` dialog box, specify the
following values:
+---------------------------------------+-------------------------------+
| :guilabel:`Template Source` | Choose the source of the |
| | template from the list. |
+---------------------------------------+-------------------------------+
| :guilabel:`Template URL/File/Data` | Depending on the source that |
| | you select, enter the URL, |
| | browse to the file location, |
| | or directly include the |
| | template. |
+---------------------------------------+-------------------------------+
| :guilabel:`Environment Source` | Choose the source of the |
| | environment from the list. |
| | The environment files contain |
| | additional settings for the |
| | stack. |
+---------------------------------------+-------------------------------+
| :guilabel:`Environment File/Data` | Depending on the source that |
| | you select, browse to the |
| | file location, directly |
| | include the environment |
+---------------------------------------+-------------------------------+
#. Click :guilabel:`Next`.
#. In the :guilabel:`Launch Stack` dialog box, specify the
following values:
+---------------------------------+---------------------------------+
| :guilabel:`Stack Name` | Enter a name to identify |
| | the stack. |
+---------------------------------+---------------------------------+
| :guilabel:`Creation Timeout` | Specify the number of minutes |
| :guilabel:`(minutes)` | that can elapse before the |
| | launch of the stack times out. |
+---------------------------------+---------------------------------+
| :guilabel:`Rollback On Failure` | Select this check box if you |
| | want the service to roll back |
| | changes if the stack fails to |
| | launch. |
+---------------------------------+---------------------------------+
| :guilabel:`Password for user` | Specify the password that |
| :guilabel:`"demo"` | the default user uses when the |
| | stack is created. |
+---------------------------------+---------------------------------+
| :guilabel:`DBUsername` | Specify the name of the |
| | database user. |
+---------------------------------+---------------------------------+
| :guilabel:`LinuxDistribution` | Specify the Linux distribution |
| | that is used in the stack. |
+---------------------------------+---------------------------------+
| :guilabel:`DBRootPassword` | Specify the root password for |
| | the database. |
+---------------------------------+---------------------------------+
| :guilabel:`KeyName` | Specify the name of the key pair|
| | to use to log in to the stack. |
+---------------------------------+---------------------------------+
| :guilabel:`DBName` | Specify the name of the |
| | database. |
+---------------------------------+---------------------------------+
| :guilabel:`DBPassword` | Specify the password of the |
| | database. |
+---------------------------------+---------------------------------+
| :guilabel:`InstanceType` | Specify the flavor for the |
| | instance. |
+---------------------------------+---------------------------------+
#. Click :guilabel:`Launch` to create a stack. The :guilabel:`Stacks`
tab shows the stack.
After the stack is created, click on the stack name to see the
following details:
Topology
The topology of the stack.
Overview
The parameters and details of the stack.
Resources
The resources used by the stack.
Events
The events related to the stack.
Template
The template for the stack.
Manage a stack
~~~~~~~~~~~~~~
#. Log in to the dashboard.
#. Select the appropriate project from the drop down menu at the top left.
#. On the :guilabel:`Project` tab, open the :guilabel:`Orchestration` tab and
click :guilabel:`Stacks` category.
#. Select the stack that you want to update.
#. Click :guilabel:`Change Stack Template`.
#. In the :guilabel:`Select Template` dialog box, select the
new template source or environment source.
#. Click :guilabel:`Next`.
The :guilabel:`Update Stack Parameters` window appears.
#. Enter new values for any parameters that you want to update.
#. Click :guilabel:`Update`.
Delete a stack
~~~~~~~~~~~~~~
When you delete a stack, you cannot undo this action.
#. Log in to the dashboard.
#. Select the appropriate project from the drop down menu at the top left.
#. On the :guilabel:`Project` tab, open the :guilabel:`Orchestration` tab and
click :guilabel:`Stacks` category.
#. Select the stack that you want to delete.
#. Click :guilabel:`Delete Stack`.
#. In the confirmation dialog box, click :guilabel:`Delete Stack`
to confirm the deletion.

View File

@ -34,7 +34,6 @@ Keystone/Nova/Glance/Swift et. al.
from openstack_dashboard.api import base
from openstack_dashboard.api import cinder
from openstack_dashboard.api import glance
from openstack_dashboard.api import heat
from openstack_dashboard.api import keystone
from openstack_dashboard.api import network
from openstack_dashboard.api import neutron
@ -46,7 +45,6 @@ __all__ = [
"base",
"cinder",
"glance",
"heat",
"keystone",
"network",
"neutron",

View File

@ -1,265 +0,0 @@
# 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.conf import settings
from heatclient import client as heat_client
from heatclient.common import template_format
from heatclient.common import template_utils
from heatclient.common import utils as heat_utils
from oslo_serialization import jsonutils
import six
from six.moves.urllib import request
from horizon import exceptions
from horizon.utils import functions as utils
from horizon.utils.memoized import memoized
from openstack_dashboard.api import base
from openstack_dashboard.contrib.developer.profiler import api as profiler
def format_parameters(params):
parameters = {}
for count, p in enumerate(params, 1):
parameters['Parameters.member.%d.ParameterKey' % count] = p
parameters['Parameters.member.%d.ParameterValue' % count] = params[p]
return parameters
@memoized
def heatclient(request, password=None):
api_version = "1"
insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
endpoint = base.url_for(request, 'orchestration')
kwargs = {
'token': request.user.token.id,
'insecure': insecure,
'ca_file': cacert,
'username': request.user.username,
'password': password
# 'timeout': args.timeout,
# 'ca_file': args.ca_file,
# 'cert_file': args.cert_file,
# 'key_file': args.key_file,
}
client = heat_client.Client(api_version, endpoint, **kwargs)
client.format_parameters = format_parameters
return client
@profiler.trace
def stacks_list(request, marker=None, sort_dir='desc', sort_key='created_at',
paginate=False, filters=None):
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
page_size = utils.get_page_size(request)
if paginate:
request_size = page_size + 1
else:
request_size = limit
kwargs = {'sort_dir': sort_dir, 'sort_key': sort_key}
if marker:
kwargs['marker'] = marker
if filters:
kwargs.update(filters)
if 'status' in kwargs:
kwargs['status'] = kwargs['status'].replace(' ', '_').upper()
stacks_iter = heatclient(request).stacks.list(limit=request_size,
**kwargs)
has_prev_data = False
has_more_data = False
stacks = list(stacks_iter)
if paginate:
if len(stacks) > page_size:
stacks.pop()
has_more_data = True
if marker is not None:
has_prev_data = True
elif sort_dir == 'asc' and marker is not None:
has_more_data = True
elif marker is not None:
has_prev_data = True
return (stacks, has_more_data, has_prev_data)
def _ignore_if(key, value):
if key != 'get_file' and key != 'type':
return True
if not isinstance(value, six.string_types):
return True
if (key == 'type' and
not value.endswith(('.yaml', '.template'))):
return True
return False
@profiler.trace
def get_template_files(template_data=None, template_url=None):
if template_data:
tpl = template_data
elif template_url:
with contextlib.closing(request.urlopen(template_url)) as u:
tpl = u.read()
else:
return {}, None
if not tpl:
return {}, None
if isinstance(tpl, six.binary_type):
tpl = tpl.decode('utf-8')
template = template_format.parse(tpl)
files = {}
_get_file_contents(template, files)
return files, template
def _get_file_contents(from_data, files):
if not isinstance(from_data, (dict, list)):
return
if isinstance(from_data, dict):
recurse_data = from_data.values()
for key, value in from_data.items():
if _ignore_if(key, value):
continue
if not value.startswith(('http://', 'https://')):
raise exceptions.GetFileError(value, 'get_file')
if value not in files:
file_content = heat_utils.read_url_content(value)
if template_utils.is_template(file_content):
template = get_template_files(template_url=value)[1]
file_content = jsonutils.dumps(template)
files[value] = file_content
else:
recurse_data = from_data
for value in recurse_data:
_get_file_contents(value, files)
@profiler.trace
def stack_delete(request, stack_id):
return heatclient(request).stacks.delete(stack_id)
@profiler.trace
def stack_get(request, stack_id):
return heatclient(request).stacks.get(stack_id)
@profiler.trace
def template_get(request, stack_id):
return heatclient(request).stacks.template(stack_id)
@profiler.trace
def stack_create(request, password=None, **kwargs):
return heatclient(request, password).stacks.create(**kwargs)
@profiler.trace
def stack_preview(request, password=None, **kwargs):
return heatclient(request, password).stacks.preview(**kwargs)
@profiler.trace
def stack_update(request, stack_id, password=None, **kwargs):
return heatclient(request, password).stacks.update(stack_id, **kwargs)
@profiler.trace
def snapshot_create(request, stack_id):
return heatclient(request).stacks.snapshot(stack_id)
@profiler.trace
def snapshot_list(request, stack_id):
return heatclient(request).stacks.snapshot_list(stack_id)
@profiler.trace
def snapshot_show(request, stack_id, snapshot_id):
return heatclient(request).stacks.snapshot_show(stack_id, snapshot_id)
@profiler.trace
def snapshot_delete(request, stack_id, snapshot_id):
return heatclient(request).stacks.snapshot_delete(stack_id, snapshot_id)
@profiler.trace
def events_list(request, stack_name):
return heatclient(request).events.list(stack_name)
@profiler.trace
def resources_list(request, stack_name):
return heatclient(request).resources.list(stack_name)
@profiler.trace
def resource_get(request, stack_id, resource_name):
return heatclient(request).resources.get(stack_id, resource_name)
@profiler.trace
def resource_metadata_get(request, stack_id, resource_name):
return heatclient(request).resources.metadata(stack_id, resource_name)
@profiler.trace
def template_validate(request, **kwargs):
return heatclient(request).stacks.validate(**kwargs)
@profiler.trace
def action_check(request, stack_id):
return heatclient(request).actions.check(stack_id)
@profiler.trace
def action_suspend(request, stack_id):
return heatclient(request).actions.suspend(stack_id)
@profiler.trace
def action_resume(request, stack_id):
return heatclient(request).actions.resume(stack_id)
@profiler.trace
def resource_types_list(request, filters=None):
return heatclient(request).resource_types.list(filters=filters)
@profiler.trace
def resource_type_get(request, resource_type):
return heatclient(request).resource_types.get(resource_type)
@profiler.trace
def service_list(request):
return heatclient(request).services.list()
@profiler.trace
def template_version_list(request):
return heatclient(request).template_versions.list()
@profiler.trace
def template_function_list(request, template_version):
return heatclient(request).template_versions.get(template_version)

View File

@ -24,7 +24,6 @@ in https://wiki.openstack.org/wiki/APIChangeGuidelines.
from openstack_dashboard.api.rest import cinder
from openstack_dashboard.api.rest import config
from openstack_dashboard.api.rest import glance
from openstack_dashboard.api.rest import heat
from openstack_dashboard.api.rest import keystone
from openstack_dashboard.api.rest import network
from openstack_dashboard.api.rest import neutron
@ -37,7 +36,6 @@ __all__ = [
'cinder',
'config',
'glance',
'heat',
'keystone',
'network',
'neutron',

View File

@ -1,51 +0,0 @@
# 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.
"""API for the heat service."""
from django.views import generic
from openstack_dashboard import api
from openstack_dashboard.api.rest import urls
from openstack_dashboard.api.rest import utils as rest_utils
@urls.register
class Validate(generic.View):
"""API for validating a template"""
url_regex = r'heat/validate/$'
@rest_utils.ajax(data_required=True)
def post(self, request):
"""Validate a template
The following parameters may be passed in the POST
application/json object. The parameters are:
request:
:param template_url: The template to validate
"""
return api.heat.template_validate(request, **(request.DATA))
@urls.register
class Services(generic.View):
"""API for heat services."""
url_regex = r'heat/services/$'
@rest_utils.ajax()
def get(self, request):
"""Get a list of heat services."""
if api.base.is_service_enabled(request, 'orchestration'):
result = api.heat.service_list(request)
return {'items': [u.to_dict() for u in result]}
else:
raise rest_utils.AjaxError(501, '')

View File

@ -1,92 +0,0 @@
{
"context_is_admin": "role:admin",
"deny_stack_user": "not role:heat_stack_user",
"deny_everybody": "!",
"cloudformation:ListStacks": "rule:deny_stack_user",
"cloudformation:CreateStack": "rule:deny_stack_user",
"cloudformation:DescribeStacks": "rule:deny_stack_user",
"cloudformation:DeleteStack": "rule:deny_stack_user",
"cloudformation:UpdateStack": "rule:deny_stack_user",
"cloudformation:CancelUpdateStack": "rule:deny_stack_user",
"cloudformation:DescribeStackEvents": "rule:deny_stack_user",
"cloudformation:ValidateTemplate": "rule:deny_stack_user",
"cloudformation:GetTemplate": "rule:deny_stack_user",
"cloudformation:EstimateTemplateCost": "rule:deny_stack_user",
"cloudformation:DescribeStackResource": "",
"cloudformation:DescribeStackResources": "rule:deny_stack_user",
"cloudformation:ListStackResources": "rule:deny_stack_user",
"cloudwatch:DeleteAlarms": "rule:deny_stack_user",
"cloudwatch:DescribeAlarmHistory": "rule:deny_stack_user",
"cloudwatch:DescribeAlarms": "rule:deny_stack_user",
"cloudwatch:DescribeAlarmsForMetric": "rule:deny_stack_user",
"cloudwatch:DisableAlarmActions": "rule:deny_stack_user",
"cloudwatch:EnableAlarmActions": "rule:deny_stack_user",
"cloudwatch:GetMetricStatistics": "rule:deny_stack_user",
"cloudwatch:ListMetrics": "rule:deny_stack_user",
"cloudwatch:PutMetricAlarm": "rule:deny_stack_user",
"cloudwatch:PutMetricData": "",
"cloudwatch:SetAlarmState": "rule:deny_stack_user",
"actions:action": "rule:deny_stack_user",
"build_info:build_info": "rule:deny_stack_user",
"events:index": "rule:deny_stack_user",
"events:show": "rule:deny_stack_user",
"resource:index": "rule:deny_stack_user",
"resource:metadata": "",
"resource:signal": "",
"resource:mark_unhealthy": "rule:deny_stack_user",
"resource:show": "rule:deny_stack_user",
"stacks:abandon": "rule:deny_stack_user",
"stacks:create": "rule:deny_stack_user",
"stacks:delete": "rule:deny_stack_user",
"stacks:detail": "rule:deny_stack_user",
"stacks:export": "rule:deny_stack_user",
"stacks:generate_template": "rule:deny_stack_user",
"stacks:global_index": "rule:deny_everybody",
"stacks:index": "rule:deny_stack_user",
"stacks:list_resource_types": "rule:deny_stack_user",
"stacks:list_template_versions": "rule:deny_stack_user",
"stacks:list_template_functions": "rule:deny_stack_user",
"stacks:lookup": "",
"stacks:preview": "rule:deny_stack_user",
"stacks:resource_schema": "rule:deny_stack_user",
"stacks:show": "rule:deny_stack_user",
"stacks:template": "rule:deny_stack_user",
"stacks:environment": "rule:deny_stack_user",
"stacks:update": "rule:deny_stack_user",
"stacks:update_patch": "rule:deny_stack_user",
"stacks:preview_update": "rule:deny_stack_user",
"stacks:preview_update_patch": "rule:deny_stack_user",
"stacks:validate_template": "rule:deny_stack_user",
"stacks:snapshot": "rule:deny_stack_user",
"stacks:show_snapshot": "rule:deny_stack_user",
"stacks:delete_snapshot": "rule:deny_stack_user",
"stacks:list_snapshots": "rule:deny_stack_user",
"stacks:restore_snapshot": "rule:deny_stack_user",
"stacks:list_outputs": "rule:deny_stack_user",
"stacks:show_output": "rule:deny_stack_user",
"software_configs:global_index": "rule:deny_everybody",
"software_configs:index": "rule:deny_stack_user",
"software_configs:create": "rule:deny_stack_user",
"software_configs:show": "rule:deny_stack_user",
"software_configs:delete": "rule:deny_stack_user",
"software_deployments:index": "rule:deny_stack_user",
"software_deployments:create": "rule:deny_stack_user",
"software_deployments:show": "rule:deny_stack_user",
"software_deployments:update": "rule:deny_stack_user",
"software_deployments:delete": "rule:deny_stack_user",
"software_deployments:metadata": "",
"service:index": "rule:context_is_admin",
"resource_types:OS::Nova::Flavor": "rule:context_is_admin",
"resource_types:OS::Cinder::EncryptedVolumeType": "rule:context_is_admin",
"resource_types:OS::Cinder::VolumeType": "rule:context_is_admin",
"resource_types:OS::Manila::ShareType": "rule:context_is_admin",
"resource_types:OS::Neutron::QoSPolicy": "rule:context_is_admin",
"resource_types:OS::Neutron::QoSBandwidthLimitRule": "rule:context_is_admin",
"resource_types:OS::Nova::HostAggregate": "rule:context_is_admin"
}

View File

@ -28,8 +28,7 @@ class Admin(horizon.Dashboard):
('image', 'context_is_admin'),
('volume', 'context_is_admin'),
('compute', 'context_is_admin'),
('network', 'context_is_admin'),
('orchestration', 'context_is_admin'),)
('network', 'context_is_admin'),)
else:
permissions = (tuple(utils.get_admin_permissions()),)

View File

@ -26,5 +26,4 @@ class Info(horizon.Panel):
slug = 'info'
policy_rules = (("compute", "context_is_admin"),
("volume", "context_is_admin"),
("network", "context_is_admin"),
("orchestration", "context_is_admin"),)
("network", "context_is_admin"),)

View File

@ -236,45 +236,3 @@ class NetworkAgentsTable(tables.DataTable):
table_actions = (NetworkAgentsFilterAction, )
row_actions = (NetworkL3AgentRoutersLinkAction, )
multi_select = False
class HeatServiceFilterAction(tables.FilterAction):
filter_field = 'type'
def filter(self, table, services, filter_string):
q = filter_string.lower()
def comp(service):
attr = getattr(service, self.filter_field, '')
if attr is not None and q in attr.lower():
return True
return False
return filter(comp, services)
class HeatServiceTable(tables.DataTable):
hostname = tables.Column('hostname', verbose_name=_('Hostname'))
binary = tables.Column("binary", verbose_name=_('Name'))
engine_id = tables.Column('engine_id', verbose_name=_('Engine Id'))
host = tables.Column('host', verbose_name=_('Host'))
topic = tables.Column('topic', verbose_name=_('Topic'))
# For consistent with other tables in system info, set column name to
# 'state'
state = tables.Column('status', verbose_name=_('State'),
display_choices=SERVICE_STATE_DISPLAY_CHOICES)
updated_at = tables.Column('updated_at',
verbose_name=pgettext_lazy(
'Time since the last update',
u'Last Updated'),
filters=(utils_filters.parse_isotime,
filters.timesince))
def get_object_id(self, obj):
return "%s" % obj.engine_id
class Meta(object):
name = "heat_services"
verbose_name = _("Orchestration Services")
table_actions = (HeatServiceFilterAction,)
multi_select = False

View File

@ -18,7 +18,6 @@ from horizon import exceptions
from horizon import tabs
from openstack_dashboard.api import base
from openstack_dashboard.api import cinder
from openstack_dashboard.api import heat
from openstack_dashboard.api import neutron
from openstack_dashboard.api import nova
from openstack_dashboard.dashboards.admin.info import constants
@ -118,32 +117,8 @@ class NetworkAgentsTab(tabs.TableTab):
return agents
class HeatServiceTab(tabs.TableTab):
table_classes = (tables.HeatServiceTable,)
name = tables.HeatServiceTable.Meta.verbose_name
slug = tables.HeatServiceTable.Meta.name
template_name = constants.INFO_DETAIL_TEMPLATE_NAME
def allowed(self, request):
try:
return base.is_service_enabled(request, 'orchestration')
except Exception:
exceptions.handle(request, _('Orchestration service is disabled.'))
return False
def get_heat_services_data(self):
try:
services = heat.service_list(self.tab_group.request)
except Exception:
msg = _('Unable to get Orchestration service list.')
exceptions.check_message(["Connection", "refused"], msg)
exceptions.handle(self.request, msg)
services = []
return services
class SystemInfoTabs(tabs.TabGroup):
slug = "system_info"
tabs = (ServicesTab, NovaServicesTab, CinderServicesTab,
NetworkAgentsTab, HeatServiceTab)
NetworkAgentsTab)
sticky = True

View File

@ -29,7 +29,7 @@ class SystemInfoViewTests(test.BaseAdminViewTests):
api.nova: ('service_list',),
api.neutron: ('agent_list', 'is_extension_supported'),
api.cinder: ('service_list',),
api.heat: ('service_list',)})
})
def _test_base_index(self):
api.base.is_service_enabled(IsA(http.HttpRequest), IgnoreArg()) \
.MultipleTimes().AndReturn(True)
@ -49,10 +49,6 @@ class SystemInfoViewTests(test.BaseAdminViewTests):
api.cinder.service_list(IsA(http.HttpRequest)).\
AndReturn(cinder_services)
heat_services = self.heat_services.list()
api.heat.service_list(IsA(http.HttpRequest)).\
AndReturn(heat_services)
self.mox.ReplayAll()
res = self.client.get(INDEX_URL)
@ -88,14 +84,3 @@ class SystemInfoViewTests(test.BaseAdminViewTests):
)
self.mox.VerifyAll()
def test_heat_index(self):
res = self._test_base_index()
heat_services_tab = res.context['tab_group'].\
get_tab('heat_services')
self.assertQuerysetEqual(
heat_services_tab._tables['heat_services'].data,
[service.__repr__() for service in self.heat_services.list()]
)
self.mox.VerifyAll()

View File

@ -1,83 +0,0 @@
# 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
from openstack_dashboard.api import heat
from openstack_dashboard.dashboards.project.stacks import mappings
from openstack_dashboard.dashboards.project.stacks import sro
class Stack(object):
pass
def d3_data(request, stack_id=''):
try:
stack = heat.stack_get(request, stack_id)
except Exception:
stack = Stack()
stack.id = stack_id
stack.stack_name = request.session.get('stack_name', '')
stack.stack_status = 'DELETE_COMPLETE'
stack.stack_status_reason = 'DELETE_COMPLETE'
try:
resources = heat.resources_list(request, stack.stack_name)
except Exception:
resources = []
d3_data = {"nodes": [], "stack": {}}
if stack:
stack_image = mappings.get_resource_image(stack.stack_status, 'stack')
stack_node = {
'stack_id': stack.id,
'name': stack.stack_name,
'status': stack.stack_status,
'image': stack_image,
'image_size': 60,
'image_x': -30,
'image_y': -30,
'text_x': 40,
'text_y': ".35em",
'in_progress': (stack.status == 'IN_PROGRESS'),
'info_box': sro.stack_info(stack, stack_image)
}
d3_data['stack'] = stack_node
if resources:
for resource in resources:
resource_image = mappings.get_resource_image(
resource.resource_status,
resource.resource_type)
resource_status = mappings.get_resource_status(
resource.resource_status)
if resource_status in ('IN_PROGRESS', 'INIT'):
in_progress = True
else:
in_progress = False
resource_node = {
'name': resource.resource_name,
'status': resource.resource_status,
'image': resource_image,
'required_by': resource.required_by,
'image_size': 50,
'image_x': -25,
'image_y': -25,
'text_x': 35,
'text_y': ".35em",
'in_progress': in_progress,
'info_box': sro.resource_info(resource)
}
d3_data['nodes'].append(resource_node)
return json.dumps(d3_data)

View File

@ -1,488 +0,0 @@
# 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 django
from django.conf import settings
from django.utils import html
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.debug import sensitive_variables
from oslo_utils import strutils
import six
from horizon import exceptions
from horizon import forms
from horizon import messages
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.images \
import utils as image_utils
from openstack_dashboard.dashboards.project.instances \
import utils as instance_utils
LOG = logging.getLogger(__name__)
def create_upload_form_attributes(prefix, input_type, name):
"""Creates attribute dicts for the switchable upload form
:type prefix: str
:param prefix: prefix (environment, template) of field
:type input_type: str
:param input_type: field type (file, raw, url)
:type name: str
:param name: translated text label to display to user
:rtype: dict
:return: an attribute set to pass to form build
"""
attributes = {'class': 'switched', 'data-switch-on': prefix + 'source'}
attributes['data-' + prefix + 'source-' + input_type] = name
return attributes
class TemplateForm(forms.SelfHandlingForm):
class Meta(object):
name = _('Select Template')
help_text = _('Select a template to launch a stack.')
# TODO(jomara) - update URL choice for template & environment files
# w/ client side download when applicable
base_choices = [('file', _('File')),
('raw', _('Direct Input'))]
url_choice = [('url', _('URL'))]
attributes = {'class': 'switchable', 'data-slug': 'templatesource'}
template_source = forms.ChoiceField(label=_('Template Source'),
choices=base_choices + url_choice,
widget=forms.ThemableSelectWidget(
attrs=attributes))
attributes = create_upload_form_attributes(
'template',
'file',
_('Template File'))
template_upload = forms.FileField(
label=_('Template File'),
help_text=_('A local template to upload.'),
widget=forms.FileInput(attrs=attributes),
required=False)
attributes = create_upload_form_attributes(
'template',
'url',
_('Template URL'))
template_url = forms.URLField(
label=_('Template URL'),
help_text=_('An external (HTTP) URL to load the template from.'),
widget=forms.TextInput(attrs=attributes),
required=False)
attributes = create_upload_form_attributes(
'template',
'raw',
_('Template Data'))
template_data = forms.CharField(
label=_('Template Data'),
help_text=_('The raw contents of the template.'),
widget=forms.widgets.Textarea(attrs=attributes),
required=False)
attributes = {'data-slug': 'envsource', 'class': 'switchable'}
environment_source = forms.ChoiceField(
label=_('Environment Source'),
choices=base_choices,
widget=forms.ThemableSelectWidget(attrs=attributes),
required=False)
attributes = create_upload_form_attributes(
'env',
'file',
_('Environment File'))
environment_upload = forms.FileField(
label=_('Environment File'),
help_text=_('A local environment to upload.'),
widget=forms.FileInput(attrs=attributes),
required=False)
attributes = create_upload_form_attributes(
'env',
'raw',
_('Environment Data'))
environment_data = forms.CharField(
label=_('Environment Data'),
help_text=_('The raw contents of the environment file.'),
widget=forms.widgets.Textarea(attrs=attributes),
required=False)
if django.VERSION >= (1, 9):
# Note(Itxaka): On django>=1.9 Charfield has an strip option that
# we need to set to False as to not hit
# https://bugs.launchpad.net/python-heatclient/+bug/1546166
environment_data.strip = False
template_data.strip = False
def __init__(self, *args, **kwargs):
self.next_view = kwargs.pop('next_view')
super(TemplateForm, self).__init__(*args, **kwargs)
def clean(self):
cleaned = super(TemplateForm, self).clean()
files = self.request.FILES
self.clean_uploaded_files('template', _('template'), cleaned, files)
self.clean_uploaded_files('environment', _('environment'), cleaned,
files)
# Validate the template and get back the params.
kwargs = {}
if cleaned['environment_data']:
kwargs['environment'] = cleaned['environment_data']
try:
files, tpl =\
api.heat.get_template_files(cleaned.get('template_data'),
cleaned.get('template_url'))
kwargs['files'] = files
kwargs['template'] = tpl
validated = api.heat.template_validate(self.request, **kwargs)
cleaned['template_validate'] = validated
cleaned['template_validate']['files'] = files
cleaned['template_validate']['template'] = tpl
except Exception as e:
raise forms.ValidationError(six.text_type(e))
return cleaned
def clean_uploaded_files(self, prefix, field_label, cleaned, files):
"""Cleans Template & Environment data from form upload.
Does some of the crunchy bits for processing uploads vs raw
data depending on what the user specified. Identical process
for environment data & template data.
:type prefix: str
:param prefix: prefix (environment, template) of field
:type field_label: str
:param field_label: translated prefix str for messages
:type input_type: dict
:param prefix: existing cleaned fields from form
:rtype: dict
:return: cleaned dict including environment & template data
"""
upload_str = prefix + "_upload"
data_str = prefix + "_data"
url = cleaned.get(prefix + '_url')
data = cleaned.get(prefix + '_data')
has_upload = upload_str in files
# Uploaded file handler
if has_upload and not url:
log_template_name = files[upload_str].name
LOG.info('got upload %s', log_template_name)
tpl = files[upload_str].read()
if tpl.startswith('{'):
try:
json.loads(tpl)
except Exception as e:
msg = _('There was a problem parsing the'
' %(prefix)s: %(error)s')
msg = msg % {'prefix': prefix, 'error': six.text_type(e)}
raise forms.ValidationError(msg)
cleaned[data_str] = tpl
# URL handler
elif url and (has_upload or data):
msg = _('Please specify a %s using only one source method.')
msg = msg % field_label
raise forms.ValidationError(msg)
elif prefix == 'template':
# Check for raw template input - blank environment allowed
if not url and not data:
msg = _('You must specify a template via one of the '
'available sources.')
raise forms.ValidationError(msg)
def create_kwargs(self, data):
kwargs = {'parameters': data['template_validate'],
'environment_data': data['environment_data']}
if data.get('stack_id'):
kwargs['stack_id'] = data['stack_id']
return kwargs
def handle(self, request, data):
kwargs = self.create_kwargs(data)
# NOTE (gabriel): This is a bit of a hack, essentially rewriting this
# request so that we can chain it as an input to the next view...
# but hey, it totally works.
request.method = 'GET'
return self.next_view.as_view()(request, **kwargs)
class ChangeTemplateForm(TemplateForm):
class Meta(object):
name = _('Edit Template')
help_text = _('Select a new template to re-launch a stack.')
stack_id = forms.CharField(label=_('Stack ID'),
widget=forms.widgets.HiddenInput)
stack_name = forms.CharField(label=_('Stack Name'),
widget=forms.TextInput(attrs={'readonly':
'readonly'}))
class PreviewTemplateForm(TemplateForm):
class Meta(object):
name = _('Preview Template')
help_text = _('Select a new template to preview a stack.')
class CreateStackForm(forms.SelfHandlingForm):
param_prefix = '__param_'
class Meta(object):
name = _('Create Stack')
environment_data = forms.CharField(
widget=forms.widgets.HiddenInput,
required=False)
if django.VERSION >= (1, 9):
# Note(Itxaka): On django>=1.9 Charfield has an strip option that
# we need to set to False as to not hit
# https://bugs.launchpad.net/python-heatclient/+bug/1546166
environment_data.strip = False
parameters = forms.CharField(
widget=forms.widgets.HiddenInput)
stack_name = forms.RegexField(
max_length=255,
label=_('Stack Name'),
help_text=_('Name of the stack to create.'),
regex=r"^[a-zA-Z][a-zA-Z0-9_.-]*$",
error_messages={'invalid':
_('Name must start with a letter and may '
'only contain letters, numbers, underscores, '
'periods and hyphens.')})
timeout_mins = forms.IntegerField(
initial=60,
label=_('Creation Timeout (minutes)'),
help_text=_('Stack creation timeout in minutes.'))
enable_rollback = forms.BooleanField(
label=_('Rollback On Failure'),
help_text=_('Enable rollback on create/update failure.'),
required=False)
def __init__(self, *args, **kwargs):
parameters = kwargs.pop('parameters')
# special case: load template data from API, not passed in params
if kwargs.get('validate_me'):
parameters = kwargs.pop('validate_me')
super(CreateStackForm, self).__init__(*args, **kwargs)
if self._stack_password_enabled():
self.fields['password'] = forms.CharField(
label=_('Password for user "%s"') % self.request.user.username,
help_text=_('This is required for operations to be performed '
'throughout the lifecycle of the stack'),
widget=forms.PasswordInput())
self._build_parameter_fields(parameters)
def _stack_password_enabled(self):
stack_settings = getattr(settings, 'OPENSTACK_HEAT_STACK', {})
return stack_settings.get('enable_user_pass', True)
def _build_parameter_fields(self, template_validate):
self.help_text = template_validate['Description']
params = template_validate.get('Parameters', {})
if template_validate.get('ParameterGroups'):
params_in_order = []
for group in template_validate['ParameterGroups']:
for param in group.get('parameters', []):
if param in params:
params_in_order.append((param, params[param]))
else:
# no parameter groups, simply sorted to make the order fixed
params_in_order = sorted(params.items())
for param_key, param in params_in_order:
field = None
field_key = self.param_prefix + param_key
initial = param.get('Value',
param.get('DefaultValue',
param.get('Default')))
field_args = {
'initial': initial,
'label': param.get('Label', param_key),
'help_text': html.escape(param.get('Description', '')),
'required': initial is None,
}
param_type = param.get('Type', None)
hidden = strutils.bool_from_string(param.get('NoEcho', 'false'))
if 'CustomConstraint' in param:
choices = self._populate_custom_choices(
param['CustomConstraint'])
field_args['choices'] = choices
field = forms.ChoiceField(**field_args)
elif 'AllowedValues' in param:
choices = map(lambda x: (x, x), param['AllowedValues'])
field_args['choices'] = choices
field = forms.ChoiceField(**field_args)
elif param_type == 'Json' and 'Default' in param:
field_args['initial'] = json.dumps(param['Default'])
field = forms.CharField(**field_args)
elif param_type in ('CommaDelimitedList', 'String', 'Json'):
if 'MinLength' in param:
field_args['min_length'] = int(param['MinLength'])
field_args['required'] = field_args['min_length'] > 0
if 'MaxLength' in param:
field_args['max_length'] = int(param['MaxLength'])
if hidden:
field_args['widget'] = forms.PasswordInput(
render_value=True)
field = forms.CharField(**field_args)
elif param_type == 'Number':
if 'MinValue' in param:
field_args['min_value'] = int(param['MinValue'])
if 'MaxValue' in param:
field_args['max_value'] = int(param['MaxValue'])
field = forms.IntegerField(**field_args)
elif param_type == 'Boolean':
field_args['required'] = False
field = forms.BooleanField(**field_args)
if field:
self.fields[field_key] = field
@sensitive_variables('password')
def handle(self, request, data):
prefix_length = len(self.param_prefix)
params_list = [(k[prefix_length:], v) for (k, v) in data.items()
if k.startswith(self.param_prefix)]
fields = {
'stack_name': data.get('stack_name'),
'timeout_mins': data.get('timeout_mins'),
'disable_rollback': not(data.get('enable_rollback')),
'parameters': dict(params_list),
'files': json.loads(data.get('parameters')).get('files'),
'template': json.loads(data.get('parameters')).get('template')
}
if data.get('password'):
fields['password'] = data.get('password')
if data.get('environment_data'):
fields['environment'] = data.get('environment_data')
try:
api.heat.stack_create(self.request, **fields)
messages.info(request, _("Stack creation started."))
return True
except Exception:
exceptions.handle(request)
def _populate_custom_choices(self, custom_type):
if custom_type == 'neutron.network':
return instance_utils.network_field_data(self.request, True)
if custom_type == 'nova.keypair':
return instance_utils.keypair_field_data(self.request, True)
if custom_type == 'glance.image':
return image_utils.image_field_data(self.request, True)
if custom_type == 'nova.flavor':
return instance_utils.flavor_field_data(self.request, True)
return []
class EditStackForm(CreateStackForm):
class Meta(object):
name = _('Update Stack Parameters')
stack_id = forms.CharField(
label=_('Stack ID'),
widget=forms.widgets.HiddenInput)
stack_name = forms.CharField(
label=_('Stack Name'),
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
@sensitive_variables('password')
def handle(self, request, data):
prefix_length = len(self.param_prefix)
params_list = [(k[prefix_length:], v) for (k, v) in data.items()
if k.startswith(self.param_prefix)]
stack_id = data.get('stack_id')
fields = {
'stack_name': data.get('stack_name'),
'timeout_mins': data.get('timeout_mins'),
'disable_rollback': not(data.get('enable_rollback')),
'parameters': dict(params_list),
'files': json.loads(data.get('parameters')).get('files'),
'template': json.loads(data.get('parameters')).get('template')
}
if data.get('password'):
fields['password'] = data.get('password')
if data.get('environment_data'):
fields['environment'] = data.get('environment_data')
try:
api.heat.stack_update(self.request, stack_id=stack_id, **fields)
messages.info(request, _("Stack update started."))
return True
except Exception:
exceptions.handle(request)
class PreviewStackForm(CreateStackForm):
class Meta(object):
name = _('Preview Stack Parameters')
def __init__(self, *args, **kwargs):
self.next_view = kwargs.pop('next_view')
super(CreateStackForm, self).__init__(*args, **kwargs)
def handle(self, request, data):
prefix_length = len(self.param_prefix)
params_list = [(k[prefix_length:], v) for (k, v) in data.items()
if k.startswith(self.param_prefix)]
fields = {
'stack_name': data.get('stack_name'),
'timeout_mins': data.get('timeout_mins'),
'disable_rollback': not(data.get('enable_rollback')),
'parameters': dict(params_list),
'files': json.loads(data.get('parameters')).get('files'),
'template': json.loads(data.get('parameters')).get('template')
}
if data.get('environment_data'):
fields['environment'] = data.get('environment_data')
try:
stack_preview = api.heat.stack_preview(self.request, **fields)
request.method = 'GET'
return self.next_view.as_view()(request,
stack_preview=stack_preview)
except Exception:
exceptions.handle(request)

View File

@ -1,350 +0,0 @@
# 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
from django.conf import settings
from django.core.urlresolvers import reverse
from django.template.defaultfilters import register
from django.utils import html
from django.utils import safestring
import six
import six.moves.urllib.parse as urlparse
from openstack_dashboard.api import swift
LOG = logging.getLogger(__name__)
resource_urls = {
"AWS::AutoScaling::AutoScalingGroup": {
'link': 'horizon:project:stacks:detail'},
"AWS::CloudFormation::Stack": {
'link': 'horizon:project:stacks:detail'},
"AWS::EC2::Instance": {
'link': 'horizon:project:instances:detail'},
"AWS::EC2::InternetGateway": {
'link': 'horizon:project:networks:ports:detail'},
"AWS::EC2::NetworkInterface": {
'link': 'horizon:project:networks:ports:detail'},
"AWS::EC2::RouteTable": {
'link': 'horizon:project:routers:detail'},
"AWS::EC2::SecurityGroup": {
'link': 'horizon:project:security_groups:index'},
"AWS::EC2::Subnet": {
'link': 'horizon:project:networks:subnets:detail'},
"AWS::EC2::Volume": {
'link': 'horizon:project:volumes:detail'},
"AWS::EC2::VPC": {
'link': 'horizon:project:networks:detail'},
"AWS::S3::Bucket": {
'link': 'horizon:project:containers:index'},
"OS::Cinder::Volume": {
'link': 'horizon:project:volumes:detail'},
"OS::Heat::AccessPolicy": {
'link': 'horizon:project:stacks:detail'},
"OS::Heat::AutoScalingGroup": {
'link': 'horizon:project:stacks:detail'},
"OS::Heat::CloudConfig": {
'link': 'horizon:project:stacks:detail'},
"OS::Neutron::Firewall": {
'link': 'horizon:project:firewalls:firewalldetails'},
"OS::Neutron::FirewallPolicy": {
'link': 'horizon:project:firewalls:policydetails'},
"OS::Neutron::FirewallRule": {
'link': 'horizon:project:firewalls:ruledetails'},
"OS::Heat::HARestarter": {
'link': 'horizon:project:stacks:detail'},
"OS::Heat::InstanceGroup": {
'link': 'horizon:project:stacks:detail'},
"OS::Heat::MultipartMime": {
'link': 'horizon:project:stacks:detail'},
"OS::Heat::ResourceGroup": {
'link': 'horizon:project:stacks:detail'},
"OS::Heat::SoftwareConfig": {
'link': 'horizon:project:stacks:detail'},
"OS::Heat::StructuredConfig": {
'link': 'horizon:project:stacks:detail'},
"OS::Heat::StructuredDeployment": {
'link': 'horizon:project:stacks:detail'},
"OS::Heat::Stack": {
'link': 'horizon:project:stacks:detail'},
"OS::Heat::WaitCondition": {
'link': 'horizon:project:stacks:detail'},
"OS::Heat::WaitConditionHandle": {
'link': 'horizon:project:stacks:detail'},
"OS::Neutron::IKEPolicy": {
'link': 'horizon:project:vpn:ikepolicydetails'},
"OS::Neutron::IPsecPolicy": {
'link': 'horizon:project:vpn:ipsecpolicydetails'},
"OS::Neutron::IPsecSiteConnection": {
'link': 'horizon:project:vpn:ipsecsiteconnectiondetails'},
"OS::Neutron::Net": {
'link': 'horizon:project:networks:detail'},
"OS::Neutron::Port": {
'link': 'horizon:project:networks:ports:detail'},
"OS::Neutron::Router": {
'link': 'horizon:project:routers:detail'},
"OS::Neutron::Subnet": {
'link': 'horizon:project:networks:subnets:detail'},
"OS::Neutron::VPNService": {
'link': 'horizon:project:vpn:vpnservicedetails'},
"OS::Nova::KeyPair": {
'link': 'horizon:project:key_pairs:index'},
"OS::Nova::Server": {
'link': 'horizon:project:instances:detail'},
"OS::Swift::Container": {
'link': 'horizon:project:containers:index',
'format_pattern': '%s' + swift.FOLDER_DELIMITER},
}
def resource_to_url(resource):
if (not resource or
not resource.physical_resource_id or
not hasattr(resource, 'resource_type')):
return None
mapping = resource_urls.get(resource.resource_type, {})
try:
if 'link' not in mapping:
return None
format_pattern = mapping.get('format_pattern') or '%s'
rid = format_pattern % resource.physical_resource_id
url = reverse(mapping['link'], args=(rid,))
except Exception as e:
LOG.exception(e)
return None
return url
@register.filter
def stack_output(output):
if not output:
return u''
if isinstance(output, six.string_types):
parts = urlparse.urlsplit(output)
if parts.netloc and parts.scheme in ('http', 'https'):
url = html.escape(output)
safe_link = u'<a href="%s" target="_blank">%s</a>' % (url, url)
return safestring.mark_safe(safe_link)
if isinstance(output, dict) or isinstance(output, list):
output = json.dumps(output, indent=2)
return safestring.mark_safe(u'<pre>%s</pre>' % html.escape(output))
static_url = getattr(settings, "STATIC_URL", "/static/")
resource_images = {
'LB_FAILED': static_url + 'dashboard/img/lb-red.svg',
'LB_DELETE': static_url + 'dashboard/img/lb-red.svg',
'LB_IN_PROGRESS': static_url + 'dashboard/img/lb-gray.gif',
'LB_INIT': static_url + 'dashboard/img/lb-gray.svg',
'LB_COMPLETE': static_url + 'dashboard/img/lb-green.svg',
'DB_FAILED': static_url + 'dashboard/img/db-red.svg',
'DB_DELETE': static_url + 'dashboard/img/db-red.svg',
'DB_IN_PROGRESS': static_url + 'dashboard/img/db-gray.gif',
'DB_INIT': static_url + 'dashboard/img/db-gray.svg',
'DB_COMPLETE': static_url + 'dashboard/img/db-green.svg',
'STACK_FAILED': static_url + 'dashboard/img/stack-red.svg',
'STACK_DELETE': static_url + 'dashboard/img/stack-red.svg',
'STACK_IN_PROGRESS': static_url + 'dashboard/img/stack-gray.gif',
'STACK_INIT': static_url + 'dashboard/img/stack-gray.svg',
'STACK_COMPLETE': static_url + 'dashboard/img/stack-green.svg',
'SERVER_FAILED': static_url + 'dashboard/img/server-red.svg',
'SERVER_DELETE': static_url + 'dashboard/img/server-red.svg',
'SERVER_IN_PROGRESS': static_url + 'dashboard/img/server-gray.gif',
'SERVER_INIT': static_url + 'dashboard/img/server-gray.svg',
'SERVER_COMPLETE': static_url + 'dashboard/img/server-green.svg',
'ALARM_FAILED': static_url + 'dashboard/img/alarm-red.svg',
'ALARM_DELETE': static_url + 'dashboard/img/alarm-red.svg',
'ALARM_IN_PROGRESS': static_url + 'dashboard/img/alarm-gray.gif',
'ALARM_INIT': static_url + 'dashboard/img/alarm-gray.svg',
'ALARM_COMPLETE': static_url + 'dashboard/img/alarm-green.svg',
'VOLUME_FAILED': static_url + 'dashboard/img/volume-red.svg',
'VOLUME_DELETE': static_url + 'dashboard/img/volume-red.svg',
'VOLUME_IN_PROGRESS': static_url + 'dashboard/img/volume-gray.gif',
'VOLUME_INIT': static_url + 'dashboard/img/volume-gray.svg',
'VOLUME_COMPLETE': static_url + 'dashboard/img/volume-green.svg',
'IMAGE_FAILED': static_url + 'dashboard/img/image-red.svg',
'IMAGE_DELETE': static_url + 'dashboard/img/image-red.svg',
'IMAGE_IN_PROGRESS': static_url + 'dashboard/img/image-gray.gif',
'IMAGE_INIT': static_url + 'dashboard/img/image-gray.svg',
'IMAGE_COMPLETE': static_url + 'dashboard/img/image-green.svg',
'WAIT_FAILED': static_url + 'dashboard/img/wait-red.svg',
'WAIT_DELETE': static_url + 'dashboard/img/wait-red.svg',
'WAIT_IN_PROGRESS': static_url + 'dashboard/img/wait-gray.gif',
'WAIT_INIT': static_url + 'dashboard/img/wait-gray.svg',
'WAIT_COMPLETE': static_url + 'dashboard/img/wait-green.svg',
'FIREWALL_FAILED': static_url + 'dashboard/img/firewall-red.svg',
'FIREWALL_DELETE': static_url + 'dashboard/img/firewall-red.svg',
'FIREWALL_IN_PROGRESS': static_url + 'dashboard/img/firewall-gray.gif',
'FIREWALL_INIT': static_url + 'dashboard/img/firewall-gray.svg',
'FIREWALL_COMPLETE': static_url + 'dashboard/img/firewall-green.svg',
'FLOATINGIP_FAILED': static_url + 'dashboard/img/floatingip-red.svg',
'FLOATINGIP_DELETE': static_url + 'dashboard/img/floatingip-red.svg',
'FLOATINGIP_IN_PROGRESS': static_url + 'dashboard/img/floatingip-gray.gif',
'FLOATINGIP_INIT': static_url + 'dashboard/img/floatingip-gray.svg',
'FLOATINGIP_COMPLETE': static_url + 'dashboard/img/floatingip-green.svg',
'ROUTER_FAILED': static_url + 'dashboard/img/router-red.svg',
'ROUTER_DELETE': static_url + 'dashboard/img/router-red.svg',
'ROUTER_IN_PROGRESS': static_url + 'dashboard/img/router-gray.gif',
'ROUTER_INIT': static_url + 'dashboard/img/router-gray.svg',
'ROUTER_COMPLETE': static_url + 'dashboard/img/router-green.svg',
'POLICY_FAILED': static_url + 'dashboard/img/policy-red.svg',
'POLICY_DELETE': static_url + 'dashboard/img/policy-red.svg',
'POLICY_IN_PROGRESS': static_url + 'dashboard/img/policy-gray.gif',
'POLICY_INIT': static_url + 'dashboard/img/policy-gray.svg',
'POLICY_COMPLETE': static_url + 'dashboard/img/policy-green.svg',
'CONFIG_FAILED': static_url + 'dashboard/img/config-red.svg',
'CONFIG_DELETE': static_url + 'dashboard/img/config-red.svg',
'CONFIG_IN_PROGRESS': static_url + 'dashboard/img/config-gray.gif',
'CONFIG_INIT': static_url + 'dashboard/img/config-gray.svg',
'CONFIG_COMPLETE': static_url + 'dashboard/img/config-green.svg',
'NETWORK_FAILED': static_url + 'dashboard/img/network-red.svg',
'NETWORK_DELETE': static_url + 'dashboard/img/network-red.svg',
'NETWORK_IN_PROGRESS': static_url + 'dashboard/img/network-gray.gif',
'NETWORK_INIT': static_url + 'dashboard/img/network-gray.svg',
'NETWORK_COMPLETE': static_url + 'dashboard/img/network-green.svg',
'PORT_FAILED': static_url + 'dashboard/img/port-red.svg',
'PORT_DELETE': static_url + 'dashboard/img/port-red.svg',
'PORT_IN_PROGRESS': static_url + 'dashboard/img/port-gray.gif',
'PORT_INIT': static_url + 'dashboard/img/port-gray.svg',
'PORT_COMPLETE': static_url + 'dashboard/img/port-green.svg',
'SECURITYGROUP_FAILED': static_url + 'dashboard/img/securitygroup-red.svg',
'SECURITYGROUP_DELETE': static_url + 'dashboard/img/securitygroup-red.svg',
'SECURITYGROUP_IN_PROGRESS':
static_url + 'dashboard/img/securitygroup-gray.gif',
'SECURITYGROUP_INIT': static_url + 'dashboard/img/securitygroup-gray.svg',
'SECURITYGROUP_COMPLETE':
static_url + 'dashboard/img/securitygroup-green.svg',
'VPN_FAILED': static_url + 'dashboard/img/vpn-red.svg',
'VPN_DELETE': static_url + 'dashboard/img/vpn-red.svg',
'VPN_IN_PROGRESS': static_url + 'dashboard/img/vpn-gray.gif',
'VPN_INIT': static_url + 'dashboard/img/vpn-gray.svg',
'VPN_COMPLETE': static_url + 'dashboard/img/vpn-green.svg',
'FLAVOR_FAILED': static_url + 'dashboard/img/flavor-red.svg',
'FLAVOR_DELETE': static_url + 'dashboard/img/flavor-red.svg',
'FLAVOR_IN_PROGRESS': static_url + 'dashboard/img/flavor-gray.gif',
'FLAVOR_INIT': static_url + 'dashboard/img/flavor-gray.svg',
'FLAVOR_COMPLETE': static_url + 'dashboard/img/flavor-green.svg',
'KEYPAIR_FAILED': static_url + 'dashboard/img/keypair-red.svg',
'KEYPAIR_DELETE': static_url + 'dashboard/img/keypair-red.svg',
'KEYPAIR_IN_PROGRESS': static_url + 'dashboard/img/keypair-gray.gif',
'KEYPAIR_INIT': static_url + 'dashboard/img/keypair-gray.svg',
'KEYPAIR_COMPLETE': static_url + 'dashboard/img/keypair-green.svg',
'UNKNOWN_FAILED': static_url + 'dashboard/img/unknown-red.svg',
'UNKNOWN_DELETE': static_url + 'dashboard/img/unknown-red.svg',
'UNKNOWN_IN_PROGRESS': static_url + 'dashboard/img/unknown-gray.gif',
'UNKNOWN_INIT': static_url + 'dashboard/img/unknown-gray.svg',
'UNKNOWN_COMPLETE': static_url + 'dashboard/img/unknown-green.svg',
}
resource_types = {
# LB
'LoadBalance': 'LB',
'HealthMonitor': 'LB',
'PoolMember': 'LB',
'Pool': 'LB',
# DB
'DBInstance': 'DB',
'Database': 'DB',
# SERVER
'Instance': 'SERVER',
'Server': 'SERVER',
# ALARM
'Alarm': 'ALARM',
'CombinationAlarm': 'ALARM',
'CWLiteAlarm': 'ALARM',
# VOLUME
'Volume': 'VOLUME',
'VolumeAttachment': 'VOLUME',
# STACK
'stack': 'STACK',
'AutoScalingGroup': 'STACK',
'InstanceGroup': 'STACK',
'ServerGroup': 'STACK',
'ResourceGroup': 'STACK',
# IMAGE
'Image': 'IMAGE',
# WAIT
'WaitCondition': 'WAIT',
'WaitConditionHandle': 'WAIT',
'UpdateWaitConditionHandle': 'WAIT',
# FIREWALL
'Firewall': 'FIREWALL',
'FirewallPolicy': 'FIREWALL',
'FirewallRule': 'FIREWALL',
# FLOATINGIP
'FloatingIP': 'FLOATINGIP',
'FloatingIPAssociation': 'FLOATINGIP',
# ROUTER
'Router': 'ROUTER',
'RouterGateway': 'ROUTER',
'RouterInterface': 'ROUTER',
# POLICY
'ScalingPolicy': 'POLICY',
# CONFIG
'CloudConfig': 'CONFIG',
'MultipartMime': 'CONFIG',
'SoftwareConfig': 'CONFIG',
'SoftwareDeployment': 'CONFIG',
'StructuredConfig': 'CONFIG',
'StructuredDeployment': 'CONFIG',
# NETWORK
'Net': 'NETWORK',
'Subnet': 'NETWORK',
'NetworkGateway': 'NETWORK',
'ProviderNet': 'NETWORK',
# PORT
'Port': 'PORT',
# SECURITYGROUP
'SecurityGroup': 'SECURITYGROUP',
# VPN
'VPNService': 'VPN',
# FLAVOR
'Flavor': 'FLAVOR',
# KEYPAIR
'KeyPair': 'KEYPAIR',
}
def get_resource_type(type):
for key, value in resource_types.items():
if key in type:
return value
return 'UNKNOWN'
def get_resource_status(status):
if ('IN_PROGRESS' in status):
return 'IN_PROGRESS'
elif ('FAILED' in status):
return 'FAILED'
elif ('DELETE' in status):
return 'DELETE'
elif ('INIT' in status):
return 'INIT'
else:
return 'COMPLETE'
def get_resource_image(status, type):
"""Sets the image url and in_progress action sw based on status."""
resource_type = get_resource_type(type)
resource_status = get_resource_status(status)
resource_state = resource_type + "_" + resource_status
for key in resource_images:
if key == resource_state:
return resource_images.get(key)

View File

@ -1,21 +0,0 @@
# 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
class Stacks(horizon.Panel):
name = _("Stacks")
slug = "stacks"
permissions = ('openstack.services.orchestration',)

View File

@ -1,23 +0,0 @@
# 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
class ResourceTypes(horizon.Panel):
name = _("Resource Types")
slug = "stacks.resource_types"
permissions = ('openstack.services.orchestration',)
policy_rules = (("orchestration", "stacks:list_resource_types"),)

View File

@ -1,36 +0,0 @@
# 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 _
from horizon import tables
class ResourceTypesFilterAction(tables.FilterAction):
filter_type = 'server'
filter_choices = (('name', _('Type ='), True, _("Case sensitive")),)
class ResourceTypesTable(tables.DataTable):
name = tables.Column("resource_type",
verbose_name=_("Type"),
link="horizon:project:stacks.resource_types:details",)
def get_object_id(self, resource):
return resource.resource_type
class Meta(object):
name = "resource_types"
verbose_name = _("Resource Types")
table_actions = (ResourceTypesFilterAction,)
multi_select = False

View File

@ -1,32 +0,0 @@
# 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 _
from horizon import tabs
class ResourceTypeOverviewTab(tabs.Tab):
name = _("Overview")
slug = "resource_type_overview"
template_name = "project/stacks.resource_types/_details.html"
def get_context_data(self, request):
return {"r_type": self.tab_group.kwargs['rt'],
"r_type_attributes": self.tab_group.kwargs['rt_attributes'],
"r_type_properties": self.tab_group.kwargs['rt_properties']}
class ResourceTypeDetailsTabs(tabs.TabGroup):
slug = "resource_type_details"
tabs = (ResourceTypeOverviewTab,)

View File

@ -1,15 +0,0 @@
{% load i18n %}
<div class="detail">
<dl>
<dd>{{ r_type }}</dd>
</dl>
<h4>{% trans "Attributes" %}</h4>
<pre>{{ r_type_attributes }}
</pre>
<h4>{% trans "Properties" %}</h4>
<pre>{{ r_type_properties }}
</pre>
</div>

View File

@ -1,52 +0,0 @@
# 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.core.urlresolvers import reverse
from django import http
from mox3.mox import IsA
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
class ResourceTypesTests(test.TestCase):
@test.create_stubs({api.heat: ('resource_types_list',)})
def test_index(self):
filters = {}
api.heat.resource_types_list(
IsA(http.HttpRequest), filters=filters).AndReturn(
self.resource_types.list())
self.mox.ReplayAll()
res = self.client.get(
reverse('horizon:project:stacks.resource_types:index'))
self.assertTemplateUsed(
res, 'horizon/common/_data_table_view.html')
self.assertContains(res, 'AWS::CloudFormation::Stack')
@test.create_stubs({api.heat: ('resource_type_get',)})
def test_detail_view(self):
rt = self.api_resource_types.first()
api.heat.resource_type_get(
IsA(http.HttpRequest), rt['resource_type']).AndReturn(rt)
self.mox.ReplayAll()
url = reverse('horizon:project:stacks.resource_types:details',
args=[rt['resource_type']])
res = self.client.get(url)
self.assertTemplateUsed(res, 'horizon/common/_detail.html')
self.assertNoMessages()

View File

@ -1,22 +0,0 @@
# 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.urls import url
from openstack_dashboard.dashboards.project.stacks.resource_types import views
urlpatterns = [
url(r'^$', views.ResourceTypesView.as_view(), name='index'),
url(r'^(?P<resource_type>[^/]+)/$',
views.DetailView.as_view(), name='details'),
]

View File

@ -1,78 +0,0 @@
# 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 yaml
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tables
from horizon import tabs
from openstack_dashboard import api
import openstack_dashboard.dashboards.project.stacks.resource_types.tables \
as project_tables
import openstack_dashboard.dashboards.project.stacks.resource_types.tabs \
as project_tabs
class ResourceTypesView(tables.DataTableView):
table_class = project_tables.ResourceTypesTable
page_title = _("Resource Types")
def get_data(self):
try:
filters = self.get_filters()
if 'name' in filters:
filters['name'] = '.*' + filters['name']
r_types = sorted(api.heat.resource_types_list(self.request,
filters=filters),
key=lambda resource: resource.resource_type)
except Exception:
r_types = []
msg = _('Unable to retrieve stack resource types.')
exceptions.handle(self.request, msg)
return r_types
class DetailView(tabs.TabView):
tab_group_class = project_tabs.ResourceTypeDetailsTabs
template_name = 'horizon/common/_detail.html'
page_title = "{{ resource_type }}"
def get_resource_type(self, request, **kwargs):
try:
resource_type_overview = api.heat.resource_type_get(
request,
kwargs['resource_type'])
return resource_type_overview
except Exception:
msg = _('Unable to retrieve resource type details.')
exceptions.handle(request, msg, redirect=self.get_redirect_url())
def get_tabs(self, request, **kwargs):
resource_type_overview = self.get_resource_type(request, **kwargs)
r_type = resource_type_overview['resource_type']
r_type_attributes = resource_type_overview['attributes']
r_type_properties = resource_type_overview['properties']
return self.tab_group_class(
request,
rt=r_type,
rt_attributes=yaml.safe_dump(r_type_attributes, indent=2),
rt_properties=yaml.safe_dump(r_type_properties, indent=2),
**kwargs)
@staticmethod
def get_redirect_url():
return reverse('horizon:project:stacks.resources:index')

View File

@ -1,44 +0,0 @@
# 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.template.defaultfilters import title
from django.template.loader import render_to_string
from horizon.utils import filters
def stack_info(stack, stack_image):
stack.stack_status_desc = title(
filters.replace_underscores(stack.stack_status))
if stack.stack_status_reason:
stack.stack_status_reason = title(
filters.replace_underscores(stack.stack_status_reason)
)
context = {}
context['stack'] = stack
context['stack_image'] = stack_image
return render_to_string('project/stacks/_stack_info.html',
context)
def resource_info(resource):
resource.resource_status_desc = title(
filters.replace_underscores(resource.resource_status)
)
if resource.resource_status_reason:
resource.resource_status_reason = title(
filters.replace_underscores(resource.resource_status_reason)
)
context = {}
context['resource'] = resource
return render_to_string('project/stacks/_resource_info.html',
context)

View File

@ -1,413 +0,0 @@
# 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.core import urlresolvers
from django.http import Http404
from django.template.defaultfilters import title
from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from heatclient import exc
from horizon import exceptions
from horizon import messages
from horizon import tables
from horizon.utils import filters
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.stacks import mappings
class LaunchStack(tables.LinkAction):
name = "launch"
verbose_name = _("Launch Stack")
url = "horizon:project:stacks:select_template"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (("orchestration", "stacks:validate_template"),
("orchestration", "stacks:create"),)
class PreviewStack(tables.LinkAction):
name = "preview"
verbose_name = _("Preview Stack")
url = "horizon:project:stacks:preview_template"
classes = ("ajax-modal",)
icon = "eye"
policy_rules = (("orchestration", "stacks:validate_template"),
("orchestration", "stacks:preview"),)
class CheckStack(tables.BatchAction):
name = "check"
verbose_name = _("Check Stack")
policy_rules = (("orchestration", "actions:action"),)
icon = "check-square"
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Check Stack",
u"Check Stacks",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Checked Stack",
u"Checked Stacks",
count
)
def action(self, request, stack_id):
api.heat.action_check(request, stack_id)
class SuspendStack(tables.BatchAction):
name = "suspend"
verbose_name = _("Suspend Stack")
policy_rules = (("orchestration", "actions:action"),)
icon = "pause"
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Suspend Stack",
u"Suspend Stacks",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Suspended Stack",
u"Suspended Stacks",
count
)
def action(self, request, stack_id):
try:
api.heat.action_suspend(request, stack_id)
except Exception:
msg = _('Failed to suspend stack.')
exceptions.handle(request, msg)
class ResumeStack(tables.BatchAction):
name = "resume"
verbose_name = _("Resume Stack")
policy_rules = (("orchestration", "actions:action"),)
icon = "play"
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Resume Stack",
u"Resume Stacks",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Resumed Stack",
u"Resumed Stacks",
count
)
def action(self, request, stack_id):
try:
api.heat.action_resume(request, stack_id)
except Exception:
msg = _('Failed to resume stack.')
exceptions.handle(request, msg)
class ChangeStackTemplate(tables.LinkAction):
name = "edit"
verbose_name = _("Change Stack Template")
url = "horizon:project:stacks:change_template"
classes = ("ajax-modal",)
icon = "pencil"
def get_link_url(self, stack):
return urlresolvers.reverse(self.url, args=[stack.id])
class DeleteStack(tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Stack",
u"Delete Stacks",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Stack",
u"Deleted Stacks",
count
)
policy_rules = (("orchestration", "stacks:delete"),)
def delete(self, request, stack_id):
try:
api.heat.stack_delete(request, stack_id)
except Exception:
msg = _('Failed to delete stack.')
exceptions.handle(request, msg)
def allowed(self, request, stack):
if stack is not None:
return stack.stack_status != 'DELETE_COMPLETE'
return True
class StacksUpdateRow(tables.Row):
ajax = True
def can_be_selected(self, datum):
return datum.stack_status != 'DELETE_COMPLETE'
def get_data(self, request, stack_id):
try:
stack = api.heat.stack_get(request, stack_id)
if stack.stack_status == 'DELETE_COMPLETE':
# returning 404 to the ajax call removes the
# row from the table on the ui
raise Http404
return stack
except Http404:
raise
except Exception as e:
messages.error(request, e)
raise
class StacksFilterAction(tables.FilterAction):
filter_type = 'server'
filter_choices = (('name', _('Stack Name ='), True, _('Case-sensitive')),
('id', _('Stack ID ='), True),
('status', _('Status ='), True))
class StacksTable(tables.DataTable):
STATUS_CHOICES = (
("Complete", True),
("Failed", False),
)
STACK_STATUS_DISPLAY_CHOICES = (
("init_in_progress", pgettext_lazy("current status of stack",
u"Init In Progress")),
("init_complete", pgettext_lazy("current status of stack",
u"Init Complete")),
("init_failed", pgettext_lazy("current status of stack",
u"Init Failed")),
("create_in_progress", pgettext_lazy("current status of stack",
u"Create In Progress")),
("create_complete", pgettext_lazy("current status of stack",
u"Create Complete")),
("create_failed", pgettext_lazy("current status of stack",
u"Create Failed")),
("delete_in_progress", pgettext_lazy("current status of stack",
u"Delete In Progress")),
("delete_complete", pgettext_lazy("current status of stack",
u"Delete Complete")),
("delete_failed", pgettext_lazy("current status of stack",
u"Delete Failed")),
("update_in_progress", pgettext_lazy("current status of stack",
u"Update In Progress")),
("update_complete", pgettext_lazy("current status of stack",
u"Update Complete")),
("update_failed", pgettext_lazy("current status of stack",
u"Update Failed")),
("rollback_in_progress", pgettext_lazy("current status of stack",
u"Rollback In Progress")),
("rollback_complete", pgettext_lazy("current status of stack",
u"Rollback Complete")),
("rollback_failed", pgettext_lazy("current status of stack",
u"Rollback Failed")),
("suspend_in_progress", pgettext_lazy("current status of stack",
u"Suspend In Progress")),
("suspend_complete", pgettext_lazy("current status of stack",
u"Suspend Complete")),
("suspend_failed", pgettext_lazy("current status of stack",
u"Suspend Failed")),
("resume_in_progress", pgettext_lazy("current status of stack",
u"Resume In Progress")),
("resume_complete", pgettext_lazy("current status of stack",
u"Resume Complete")),
("resume_failed", pgettext_lazy("current status of stack",
u"Resume Failed")),
("adopt_in_progress", pgettext_lazy("current status of stack",
u"Adopt In Progress")),
("adopt_complete", pgettext_lazy("current status of stack",
u"Adopt Complete")),
("adopt_failed", pgettext_lazy("current status of stack",
u"Adopt Failed")),
("snapshot_in_progress", pgettext_lazy("current status of stack",
u"Snapshot In Progress")),
("snapshot_complete", pgettext_lazy("current status of stack",
u"Snapshot Complete")),
("snapshot_failed", pgettext_lazy("current status of stack",
u"Snapshot Failed")),
("check_in_progress", pgettext_lazy("current status of stack",
u"Check In Progress")),
("check_complete", pgettext_lazy("current status of stack",
u"Check Complete")),
("check_failed", pgettext_lazy("current status of stack",
u"Check Failed")),
)
name = tables.Column("stack_name",
verbose_name=_("Stack Name"),
link="horizon:project:stacks:detail",)
created = tables.Column("creation_time",
verbose_name=_("Created"),
filters=(filters.parse_isotime,
filters.timesince_sortable),
attrs={'data-type': 'timesince'})
updated = tables.Column("updated_time",
verbose_name=_("Updated"),
filters=(filters.parse_isotime,
filters.timesince_or_never))
status = tables.Column("status",
hidden=True,
status=True,
status_choices=STATUS_CHOICES)
stack_status = tables.Column("stack_status",
verbose_name=_("Status"),
display_choices=STACK_STATUS_DISPLAY_CHOICES)
def get_object_display(self, stack):
return stack.stack_name
class Meta(object):
name = "stacks"
verbose_name = _("Stacks")
pagination_param = 'stack_marker'
status_columns = ["status", ]
row_class = StacksUpdateRow
table_actions_menu = (CheckStack,
SuspendStack,
ResumeStack,)
table_actions = (LaunchStack,
PreviewStack,
DeleteStack,
StacksFilterAction,)
row_actions = (CheckStack,
SuspendStack,
ResumeStack,
ChangeStackTemplate,
DeleteStack,)
def get_resource_url(obj):
if obj.physical_resource_id == obj.stack_id:
return None
return urlresolvers.reverse('horizon:project:stacks:resource',
args=(obj.stack_id, obj.resource_name))
class EventsTable(tables.DataTable):
logical_resource = tables.Column('resource_name',
verbose_name=_("Stack Resource"),
link=get_resource_url)
physical_resource = tables.Column('physical_resource_id',
verbose_name=_("Resource"))
timestamp = tables.Column('event_time',
verbose_name=_("Time Since Event"),
filters=(filters.parse_isotime,
filters.timesince_or_never))
status = tables.Column("resource_status",
filters=(title, filters.replace_underscores),
verbose_name=_("Status"),)
statusreason = tables.Column("resource_status_reason",
verbose_name=_("Status Reason"),)
class Meta(object):
name = "events"
verbose_name = _("Stack Events")
class ResourcesUpdateRow(tables.Row):
ajax = True
def get_data(self, request, resource_name):
try:
stack = self.table.stack
stack_identifier = '%s/%s' % (stack.stack_name, stack.id)
return api.heat.resource_get(
request, stack_identifier, resource_name)
except exc.HTTPNotFound:
# returning 404 to the ajax call removes the
# row from the table on the ui
raise Http404
except Exception as e:
messages.error(request, e)
class ResourcesTable(tables.DataTable):
class StatusColumn(tables.Column):
def get_raw_data(self, datum):
return datum.resource_status.partition("_")[2]
STATUS_CHOICES = (
("Complete", True),
("Failed", False),
)
STATUS_DISPLAY_CHOICES = StacksTable.STACK_STATUS_DISPLAY_CHOICES
logical_resource = tables.Column('resource_name',
verbose_name=_("Stack Resource"),
link=get_resource_url)
physical_resource = tables.Column('physical_resource_id',
verbose_name=_("Resource"),
link=mappings.resource_to_url)
resource_type = tables.Column("resource_type",
verbose_name=_("Stack Resource Type"),)
updated_time = tables.Column('updated_time',
verbose_name=_("Date Updated"),
filters=(filters.parse_isotime,
filters.timesince_or_never))
status = tables.Column("resource_status",
verbose_name=_("Status"),
display_choices=STATUS_DISPLAY_CHOICES)
statusreason = tables.Column("resource_status_reason",
verbose_name=_("Status Reason"),)
status_hidden = StatusColumn("status",
hidden=True,
status=True,
status_choices=STATUS_CHOICES)
def __init__(self, request, data=None,
needs_form_wrapper=None, **kwargs):
super(ResourcesTable, self).__init__(
request, data, needs_form_wrapper, **kwargs)
self.stack = kwargs['stack']
def get_object_id(self, datum):
return datum.resource_name
class Meta(object):
name = "resources"
verbose_name = _("Stack Resources")
status_columns = ["status_hidden", ]
row_class = ResourcesUpdateRow

View File

@ -1,173 +0,0 @@
# 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
from django.utils.translation import ugettext_lazy as _
from horizon import messages
from horizon import tabs
from openstack_dashboard import api
from openstack_dashboard import policy
from openstack_dashboard.dashboards.project.stacks \
import api as project_api
from openstack_dashboard.dashboards.project.stacks import mappings
from openstack_dashboard.dashboards.project.stacks \
import tables as project_tables
LOG = logging.getLogger(__name__)
class StackTopologyTab(tabs.Tab):
name = _("Topology")
slug = "topology"
template_name = "project/stacks/_detail_topology.html"
preload = False
def allowed(self, request):
return policy.check(
(("orchestration", "stacks:template"),
("orchestration", "stacks:lookup"),
("orchestration", "stacks:show"),
("orchestration", "resource:index"),),
request)
def get_context_data(self, request):
context = {}
stack = self.tab_group.kwargs['stack']
context['stack_id'] = stack.id
context['d3_data'] = project_api.d3_data(request, stack_id=stack.id)
return context
class StackOverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
template_name = "project/stacks/_detail_overview.html"
def allowed(self, request):
return policy.check(
(("orchestration", "stacks:template"),
("orchestration", "stacks:lookup"),
("orchestration", "stacks:show"),),
request)
def get_context_data(self, request):
return {"stack": self.tab_group.kwargs['stack']}
class ResourceOverviewTab(tabs.Tab):
name = _("Overview")
slug = "resource_overview"
template_name = "project/stacks/_resource_overview.html"
def get_context_data(self, request):
resource = self.tab_group.kwargs['resource']
resource_url = mappings.resource_to_url(resource)
return {
"resource": resource,
"resource_url": resource_url,
"metadata": self.tab_group.kwargs['metadata']}
class StackEventsTab(tabs.Tab):
name = _("Events")
slug = "events"
template_name = "project/stacks/_detail_events.html"
preload = False
def allowed(self, request):
return policy.check(
(("orchestration", "stacks:template"),
("orchestration", "stacks:lookup"),
("orchestration", "stacks:show"),
("orchestration", "events:index"),),
request)
def get_context_data(self, request):
stack = self.tab_group.kwargs['stack']
try:
stack_identifier = '%s/%s' % (stack.stack_name, stack.id)
events = api.heat.events_list(self.request, stack_identifier)
LOG.debug('got events %s', events)
# The stack id is needed to generate the resource URL.
for event in events:
event.stack_id = stack.id
except Exception:
events = []
messages.error(request, _(
'Unable to get events for stack "%s".') % stack.stack_name)
return {"stack": stack,
"table": project_tables.EventsTable(request, data=events), }
class StackResourcesTab(tabs.Tab):
name = _("Resources")
slug = "resources"
template_name = "project/stacks/_detail_resources.html"
preload = False
def allowed(self, request):
return policy.check(
(("orchestration", "stacks:template"),
("orchestration", "stacks:lookup"),
("orchestration", "stacks:show"),
("orchestration", "resource:index"),),
request)
def get_context_data(self, request):
stack = self.tab_group.kwargs['stack']
try:
stack_identifier = '%s/%s' % (stack.stack_name, stack.id)
resources = api.heat.resources_list(self.request, stack_identifier)
LOG.debug('got resources %s', resources)
# The stack id is needed to generate the resource URL.
for r in resources:
r.stack_id = stack.id
except Exception:
resources = []
messages.error(request, _(
'Unable to get resources for stack "%s".') % stack.stack_name)
return {"stack": stack,
"table": project_tables.ResourcesTable(
request, data=resources, stack=stack), }
class StackTemplateTab(tabs.Tab):
name = _("Template")
slug = "stack_template"
template_name = "project/stacks/_stack_template.html"
def allowed(self, request):
return policy.check(
(("orchestration", "stacks:template"),
("orchestration", "stacks:lookup"),
("orchestration", "stacks:show"),),
request)
def get_context_data(self, request):
return {"stack_template": self.tab_group.kwargs['stack_template']}
class StackDetailTabs(tabs.TabGroup):
slug = "stack_details"
tabs = (StackTopologyTab, StackOverviewTab, StackResourcesTab,
StackEventsTab, StackTemplateTab)
sticky = True
class ResourceDetailTabs(tabs.TabGroup):
slug = "resource_details"
tabs = (ResourceOverviewTab,)
sticky = True

View File

@ -1,23 +0,0 @@
# 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
class TemplateVersions(horizon.Panel):
name = _("Template Versions")
slug = "stacks.template_versions"
permissions = ('openstack.services.orchestration',)
policy_rules = (("orchestration", "stacks:list_template_versions"),)

View File

@ -1,52 +0,0 @@
# 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.template import defaultfilters as filters
from django.utils.translation import ugettext_lazy as _
from horizon import tables
class TemplateVersionsTable(tables.DataTable):
version = tables.Column(
"version",
verbose_name=_("Version"),
link="horizon:project:stacks.template_versions:details",)
type = tables.Column(
"type",
verbose_name=_("Type"),
filters=(filters.upper,))
def get_object_id(self, template_versions):
return template_versions.version
class Meta(object):
name = "template_versions"
table_actions = (tables.FilterAction,)
verbose_name = _("Template Versions")
table_actions = (tables.FilterAction,)
multi_select = False
class TemplateFunctionsTable(tables.DataTable):
functions = tables.Column('functions', verbose_name=_("Function"))
description = tables.Column('description', verbose_name=_("Description"))
def get_object_id(self, template_functions):
return template_functions.functions
class Meta(object):
name = "template_functions"
verbose_name = _("Template Functions")
table_actions = (tables.FilterAction,)
multi_select = False

View File

@ -1,51 +0,0 @@
# 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 _
from horizon import messages
from horizon import tabs
from openstack_dashboard import api
from openstack_dashboard import policy
from openstack_dashboard.dashboards.project.stacks.template_versions \
import tables as project_tables
class TemplateFunctionsTab(tabs.Tab):
name = _("Template Functions")
slug = "template_functions"
template_name = "project/stacks.template_versions/_details.html"
preload = False
def allowed(self, request):
return policy.check(
(("orchestration", "stacks:list_template_functions"),),
request)
def get_context_data(self, request):
template_version = self.tab_group.kwargs['template_version']
try:
template_functions = api.heat.template_function_list(
self.request, template_version)
except Exception:
template_functions = []
messages.error(request, _('Unable to get functions for template '
'version "%s".') % template_version)
return {"table": project_tables.TemplateFunctionsTable(
request, data=template_functions), }
class TemplateVersionDetailsTabs(tabs.TabGroup):
slug = "template_version_details"
tabs = (TemplateFunctionsTab,)

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Template Versions" %}{% endblock %}
{% block main %}
{{ table.render }}
{% endblock %}

View File

@ -1,79 +0,0 @@
# 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.core.urlresolvers import reverse
from django import http
from mox3.mox import IsA
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
class TemplateVersionsTests(test.TestCase):
INDEX_URL = reverse('horizon:project:stacks.template_versions:index')
@test.create_stubs({api.heat: ('template_version_list',)})
def test_index(self):
api.heat.template_version_list(
IsA(http.HttpRequest)).AndReturn(self.template_versions.list())
self.mox.ReplayAll()
res = self.client.get(self.INDEX_URL)
self.assertTemplateUsed(
res, 'project/stacks.template_versions/index.html')
self.assertContains(res, 'HeatTemplateFormatVersion.2012-12-12')
@test.create_stubs({api.heat: ('template_version_list',)})
def test_index_exception(self):
api.heat.template_version_list(
IsA(http.HttpRequest)).AndRaise(self.exceptions.heat)
self.mox.ReplayAll()
res = self.client.get(self.INDEX_URL)
self.assertTemplateUsed(
res, 'project/stacks.template_versions/index.html')
self.assertEqual(len(res.context['table'].data), 0)
self.assertMessageCount(res, error=1)
@test.create_stubs({api.heat: ('template_function_list',)})
def test_detail_view(self):
t_version = self.template_versions.first().version
t_functions = self.template_functions.list()
api.heat.template_function_list(
IsA(http.HttpRequest), t_version).AndReturn(t_functions)
self.mox.ReplayAll()
url = reverse('horizon:project:stacks.template_versions:details',
args=[t_version])
res = self.client.get(url)
self.assertTemplateUsed(res, 'horizon/common/_detail.html')
self.assertNoMessages()
@test.create_stubs({api.heat: ('template_function_list',)})
def test_detail_view_with_exception(self):
t_version = self.template_versions.first().version
api.heat.template_function_list(
IsA(http.HttpRequest), t_version).AndRaise(self.exceptions.heat)
self.mox.ReplayAll()
url = reverse('horizon:project:stacks.template_versions:details',
args=[t_version])
res = self.client.get(url)
self.assertTemplateUsed(res, 'horizon/common/_detail.html')
self.assertEqual(len(res.context['table'].data), 0)
self.assertMessageCount(res, error=1)

View File

@ -1,24 +0,0 @@
# 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.urls import url
from openstack_dashboard.dashboards.project.stacks.template_versions \
import views
urlpatterns = [
url(r'^$', views.TemplateVersionsView.as_view(), name='index'),
url(r'^(?P<template_version>[^/]+)/$',
views.DetailView.as_view(), name='details'),
]

View File

@ -1,61 +0,0 @@
# 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.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tables
from horizon import tabs
from openstack_dashboard import api
import openstack_dashboard.dashboards.project.stacks.template_versions.tables \
as project_tables
import openstack_dashboard.dashboards.project.stacks.template_versions.tabs \
as project_tabs
class TemplateVersionsView(tables.DataTableView):
table_class = project_tables.TemplateVersionsTable
template_name = 'project/stacks.template_versions/index.html'
page_title = _("Template Versions")
def get_data(self):
try:
template_versions = sorted(
api.heat.template_version_list(self.request),
key=lambda template_version: template_version.version)
except Exception:
template_versions = []
msg = _('Unable to retrieve template versions.')
exceptions.handle(self.request, msg)
return template_versions
class DetailView(tabs.TabView):
tab_group_class = project_tabs.TemplateVersionDetailsTabs
template_name = 'horizon/common/_detail.html'
page_title = "{{ template_version }}"
def get_template_version(self, request, **kwargs):
try:
template_functions = api.heat.template_function_list(
request, kwargs['template_version'])
return template_functions
except Exception:
msg = _('Unable to retrieve template functions.')
exceptions.handle(request, msg, redirect=self.get_redirect_url())
@staticmethod
def get_redirect_url():
return reverse('horizon:project:stacks.template_versions:index')

View File

@ -1,7 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Use one of the available template source options to specify the template to be used in creating this stack." %}</p>
{% endblock %}

View File

@ -1,6 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Create a new stack with the provided values." %}</p>
{% endblock %}

View File

@ -1,3 +0,0 @@
{% load i18n %}
{{ table.render }}

View File

@ -1,55 +0,0 @@
{% load i18n sizeformat %}
<div class="detail">
<dl class="dl-horizontal">
<dt>{% trans "Name" %}</dt>
<dd data-display="{{ stack.stack_name|default:stack.id }}">{{ stack.stack_name }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>{{ stack.id }}</dd>
<dt>{% trans "Description" %}</dt>
<dd>{{ stack.description }}</dd>
</dl>
<h4>{% trans "Status" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
<dt>{% trans "Created" %}</dt>
<dd>{{ stack.creation_time|parse_isotime|timesince_or_never }}</dd>
<dt>{% trans "Last Updated" %}</dt>
<dd>{{ stack.updated_time|parse_isotime|timesince_or_never }}</dd>
<dt>{% trans "Status" %}</dt>
<dd>
{% blocktrans with stack_status_title=stack.stack_status|title stack_status_reason=stack.stack_status_reason %}{{ stack_status_title }}: {{ stack_status_reason }}{% endblocktrans %}
</dd>
</dl>
<h4>{% trans "Outputs" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
{% for output in stack.outputs %}
<dt>{{ output.output_key }}</dt>
<dd>{{ output.description }}</dd>
<dd>
{{ output.output_value|stack_output }}
</dd>
{% endfor %}
</dl>
<h4>{% trans "Stack Parameters" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
{% for key, value in stack.parameters.items %}
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
</dl>
<h4>{% trans "Launch Parameters" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
<dt>{% trans "Timeout" %}</dt>
<dd>{{ stack.timeout_mins }} {% trans "Minutes" %}</dd>
<dt>{% trans "Rollback" %}</dt>
<dd>{% if stack.disable_rollback %}{% trans "Disabled" %}{% else %}{% trans "Enabled" %}{% endif %}</dd>
</dl>
</div>

View File

@ -1,3 +0,0 @@
{% load i18n %}
{{ table.render }}

View File

@ -1,9 +0,0 @@
{% load i18n sizeformat %}
<div id="resource_container">
<div id="info_box"></div>
<div id="stack_box"></div>
<div id="heat_resource_topology"></div>
<div id="stack_id" data-stack_id="{{ stack_id }}"></div>
<div id="d3_data" data-d3_data="{{ d3_data }}"></div>
</div>

View File

@ -1,6 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Preview a new stack with the provided values." %}</p>
{% endblock %}

View File

@ -1,58 +0,0 @@
{% extends "horizon/common/_modal.html" %}
{% load i18n %}
{% block modal-header %}{% trans "Stack Preview" %}{% endblock %}
{% block modal-body %}
<div class="row-fluid stack-preview detail">
<form>
<dl class="dl-horizontal">
{% for key, value in stack_preview.items %}
{% if key != 'parameters' and key != 'resources' and key != 'links' %}
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
{% endif %}
{% endfor %}
</dl>
{% if stack_preview.parameters %}
<dt>{% trans "Parameters" %}</dt>
<hr class="header_rule">
<dl class="dl-horizontal">
{% for key, value in stack_preview.parameters.items %}
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
</dl>
{% endif %}
{% if stack_preview.links %}
<dt>{% trans "Links" %}</dt>
<hr class="header_rule">
{% for link in stack_preview.links %}
<dl class="dl-horizontal">
<dt>{{ link.rel }}</dt>
<dd>{{ link.href }}</dd>
</dl>
{% endfor %}
{% endif %}
{% if stack_preview.resources %}
<dt>{% trans "Resources" %}</dt>
{% for resource in stack_preview.resources %}
<hr class="header_rule">
<dl class="dl-horizontal">
{% for key, value in resource.items %}
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
</dl>
{% endfor %}
{% endif %}
</form>
</div>
{% endblock %}
{% block modal-footer %}
<a href="{% url 'horizon:project:stacks:index' %}" class="btn btn-default cancel">{% trans "Close" %}</a>
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Use one of the available template source options to specify the template to be used in previewing this stack." %}</p>
{% endblock %}

View File

@ -1,10 +0,0 @@
<h3>{{ resource.resource_name }}</h3>
{% if resource.resource_status == 'CREATE_FAILED' %}
<p class="error">{{ resource.resource_status_desc }}</p>
<p class="error">{{ resource.resource_status_reason }}</p>
{% else %}
<p>{{ resource.resource_status_desc }}</p>
{% endif %}
<p>{{ resource.resource_type }}</p>

View File

@ -1,38 +0,0 @@
{% load i18n sizeformat %}
<div class="detail">
<dl class="dl-horizontal">
<dt>{% trans "Stack Resource ID" %}</dt>
<dd>{{ resource.resource_name }}</dd>
<dt>{% trans "Resource ID" %}</dt>
<dd>
{% if resource_url %}
<a href="{{ resource_url }}">
{{ resource.physical_resource_id }}
</a>
{% else %}
{{ resource.physical_resource_id }}
{% endif %}
</dd>
<dt>{% trans "Stack Resource Type" %}</dt>
<dd>{{ resource.resource_type }}</dd>
<dt>{% trans "Description" %}</dt>
<dd>{{ resource.description }}</dd>
</dl>
<h4>{% trans "Status" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
<dt>{% trans "Last Updated" %}</dt>
<dd>{{ resource.updated_time|parse_isotime|timesince_or_never }}</dd>
<dt>{% trans "Status" %}</dt>
<dd>
{% blocktrans with resource_status=resource.resource_status|title|replace_underscores resource_status_reason=resource.resource_status_reason %}{{ resource_status }}: {{ resource_status_reason }}{% endblocktrans %}
</dd>
</dl>
<h4>{% trans "Resource Metadata" %}</h4>
<hr class="header_rule">
<pre>{{ metadata }}
</pre>
</div>

View File

@ -1,7 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Use one of the available template source options to specify the template to be used in creating this stack." %}</p>
{% endblock %}

View File

@ -1,14 +0,0 @@
<img src="{{ stack_image }}" width="35px" height="35px" />
<div id="stack_info">
<h3>{{ stack.stack_name }}</h3>
<p class="error">{{ stack.stack_status_desc }}</p>
</div>
<div class="clear"></div>
{% if stack.stack_status == 'CREATE_FAILED' %}
<p class="error">{{ stack.stack_status_reason }}</p>
{% endif %}
{% for output in stack.outputs %}
{% if output.output_key == 'WebsiteURL' %}
<a href="{{ output.output_value }}">{{ output.description }}</a>
{% endif %}
{% endfor %}

View File

@ -1,5 +0,0 @@
{% load i18n sizeformat %}
<div class="detail">
<pre>{{ stack_template }}</pre>
</div>

View File

@ -1,6 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Update a stack with the provided values. Please note that any encrypted parameters, such as passwords, will be reset to default if you do not change them here." %}</p>
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Change Template" %}{% endblock %}
{% block main %}
{% include 'project/stacks/_change_template.html' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Launch Stack" %}{% endblock %}
{% block main %}
{% include 'project/stacks/_create.html' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Preview Stack" %}{% endblock %}
{% block main %}
{% include 'project/stacks/_preview.html' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Preview Stack Details" %}{% endblock %}
{% block main %}
{% include 'project/stacks/_preview_details.html' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Preview Template" %}{% endblock %}
{% block main %}
{% include 'project/stacks/_preview_template.html' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Select Template" %}{% endblock %}
{% block main %}
{% include 'project/stacks/_select_template.html' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Update Stack Parameters" %}{% endblock %}
{% block main %}
{% include 'project/stacks/_update.html' %}
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +0,0 @@
# 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.urls import url
from openstack_dashboard.dashboards.project.stacks import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^select_template$',
views.SelectTemplateView.as_view(),
name='select_template'),
url(r'^launch$', views.CreateStackView.as_view(), name='launch'),
url(r'^preview_template$',
views.PreviewTemplateView.as_view(), name='preview_template'),
url(r'^preview$', views.PreviewStackView.as_view(), name='preview'),
url(r'^preview_details$',
views.PreviewStackDetailsView.as_view(), name='preview_details'),
url(r'^stack/(?P<stack_id>[^/]+)/$',
views.DetailView.as_view(), name='detail'),
url(r'^(?P<stack_id>[^/]+)/change_template$',
views.ChangeTemplateView.as_view(), name='change_template'),
url(r'^(?P<stack_id>[^/]+)/edit_stack$',
views.EditStackView.as_view(), name='edit_stack'),
url(r'^stack/(?P<stack_id>[^/]+)/(?P<resource_name>[^/]+)/$',
views.ResourceView.as_view(), name='resource'),
url(r'^get_d3_data/(?P<stack_id>[^/]+)/$',
views.JSONView.as_view(), name='d3_data'),
]

View File

@ -1,358 +0,0 @@
# 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
from operator import attrgetter
import yaml
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
from django.http import HttpResponse
from django.utils.translation import ugettext_lazy as _
import django.views.generic
from horizon import exceptions
from horizon import forms
from horizon import tables
from horizon import tabs
from horizon.utils import memoized
from horizon import views
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.stacks \
import api as project_api
from openstack_dashboard.dashboards.project.stacks \
import forms as project_forms
from openstack_dashboard.dashboards.project.stacks \
import tables as project_tables
from openstack_dashboard.dashboards.project.stacks \
import tabs as project_tabs
class IndexView(tables.DataTableView):
table_class = project_tables.StacksTable
page_title = _("Stacks")
def __init__(self, *args, **kwargs):
super(IndexView, self).__init__(*args, **kwargs)
self._more = None
def has_prev_data(self, table):
return self._prev
def has_more_data(self, table):
return self._more
def get_data(self):
stacks = []
filters = self.get_filters()
prev_marker = self.request.GET.get(
project_tables.StacksTable._meta.prev_pagination_param)
if prev_marker is not None:
sort_dir = 'asc'
marker = prev_marker
else:
sort_dir = 'desc'
marker = self.request.GET.get(
project_tables.StacksTable._meta.pagination_param)
try:
stacks, self._more, self._prev = api.heat.stacks_list(
self.request,
marker=marker,
paginate=True,
sort_dir=sort_dir,
filters=filters)
if prev_marker is not None:
stacks = sorted(stacks, key=attrgetter('creation_time'),
reverse=True)
except Exception:
self._prev = False
self._more = False
msg = _('Unable to retrieve stack list.')
exceptions.handle(self.request, msg)
return stacks
class SelectTemplateView(forms.ModalFormView):
template_name = 'project/stacks/select_template.html'
form_id = "select_template"
form_class = project_forms.TemplateForm
submit_label = _("Next")
submit_url = reverse_lazy("horizon:project:stacks:select_template")
success_url = reverse_lazy('horizon:project:stacks:launch')
page_title = _("Select Template")
def get_initial(self):
initial = {}
for name in [
'template_url',
'template_source',
'template_data',
'environment_source',
'environment_data'
]:
tmp = self.request.GET.get(name)
if tmp:
initial[name] = tmp
return initial
def get_form_kwargs(self):
kwargs = super(SelectTemplateView, self).get_form_kwargs()
kwargs['next_view'] = CreateStackView
return kwargs
class ChangeTemplateView(forms.ModalFormView):
template_name = 'project/stacks/change_template.html'
form_id = "change_template"
form_class = project_forms.ChangeTemplateForm
submit_label = _("Next")
submit_url = "horizon:project:stacks:change_template"
cancel_url = reverse_lazy('horizon:project:stacks:index')
success_url = reverse_lazy('horizon:project:stacks:edit_stack')
page_title = _("Change Template")
def get_context_data(self, **kwargs):
context = super(ChangeTemplateView, self).get_context_data(**kwargs)
args = (self.get_object().id,)
context['submit_url'] = reverse(self.submit_url, args=args)
return context
@memoized.memoized_method
def get_object(self):
stack_id = self.kwargs['stack_id']
try:
self._stack = api.heat.stack_get(self.request, stack_id)
except Exception:
msg = _("Unable to retrieve stack.")
redirect = reverse('horizon:project:stacks:index')
exceptions.handle(self.request, msg, redirect=redirect)
return self._stack
def get_initial(self):
stack = self.get_object()
return {'stack_id': stack.id,
'stack_name': stack.stack_name
}
def get_form_kwargs(self):
kwargs = super(ChangeTemplateView, self).get_form_kwargs()
kwargs['next_view'] = EditStackView
return kwargs
class PreviewTemplateView(forms.ModalFormView):
template_name = 'project/stacks/preview_template.html'
form_id = "preview_template"
form_class = project_forms.PreviewTemplateForm
submit_label = _("Next")
submit_url = reverse_lazy('horizon:project:stacks:preview_template')
success_url = reverse_lazy('horizon:project:stacks:preview')
page_title = _("Preview Template")
def get_form_kwargs(self):
kwargs = super(PreviewTemplateView, self).get_form_kwargs()
kwargs['next_view'] = PreviewStackView
return kwargs
class CreateStackView(forms.ModalFormView):
template_name = 'project/stacks/create.html'
form_id = "launch_stack"
form_class = project_forms.CreateStackForm
submit_label = _("Launch")
submit_url = reverse_lazy("horizon:project:stacks:launch")
success_url = reverse_lazy('horizon:project:stacks:index')
page_title = _("Launch Stack")
def get_initial(self):
initial = {}
if 'environment_data' in self.kwargs:
initial['environment_data'] = self.kwargs['environment_data']
if 'parameters' in self.kwargs:
initial['parameters'] = json.dumps(self.kwargs['parameters'])
return initial
def get_form_kwargs(self):
kwargs = super(CreateStackView, self).get_form_kwargs()
if 'parameters' in self.kwargs:
kwargs['parameters'] = self.kwargs['parameters']
else:
data = json.loads(self.request.POST['parameters'])
kwargs['parameters'] = data
return kwargs
# edit stack parameters, coming from template selector
class EditStackView(CreateStackView):
template_name = 'project/stacks/update.html'
form_id = "update_stack"
form_class = project_forms.EditStackForm
submit_label = _("Update")
submit_url = "horizon:project:stacks:edit_stack"
success_url = reverse_lazy('horizon:project:stacks:index')
page_title = _("Update Stack")
def get_initial(self):
initial = super(EditStackView, self).get_initial()
initial['stack'] = self.get_object()['stack']
if initial['stack']:
initial['stack_id'] = initial['stack'].id
initial['stack_name'] = initial['stack'].stack_name
return initial
def get_context_data(self, **kwargs):
context = super(EditStackView, self).get_context_data(**kwargs)
args = (self.get_object()['stack'].id,)
context['submit_url'] = reverse(self.submit_url, args=args)
return context
@memoized.memoized_method
def get_object(self):
stack_id = self.kwargs['stack_id']
try:
stack = {}
stack['stack'] = api.heat.stack_get(self.request, stack_id)
stack['template'] = api.heat.template_get(self.request, stack_id)
self._stack = stack
except Exception:
msg = _("Unable to retrieve stack.")
redirect = reverse('horizon:project:stacks:index')
exceptions.handle(self.request, msg, redirect=redirect)
return self._stack
class PreviewStackView(CreateStackView):
template_name = 'project/stacks/preview.html'
form_id = "preview_stack"
form_class = project_forms.PreviewStackForm
submit_label = _("Preview")
submit_url = reverse_lazy('horizon:project:stacks:preview')
success_url = reverse_lazy('horizon:project:stacks:index')
page_title = _("Preview Stack")
def get_form_kwargs(self):
kwargs = super(CreateStackView, self).get_form_kwargs()
kwargs['next_view'] = PreviewStackDetailsView
return kwargs
class PreviewStackDetailsView(forms.ModalFormMixin, views.HorizonTemplateView):
template_name = 'project/stacks/preview_details.html'
page_title = _("Preview Stack Details")
def get_context_data(self, **kwargs):
context = super(
PreviewStackDetailsView, self).get_context_data(**kwargs)
context['stack_preview'] = self.kwargs['stack_preview'].to_dict()
return context
class DetailView(tabs.TabView):
tab_group_class = project_tabs.StackDetailTabs
template_name = 'horizon/common/_detail.html'
page_title = "{{ stack.stack_name|default:stack.id }}"
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
stack = self.get_data(self.request, **kwargs)
table = project_tables.StacksTable(self.request)
context["stack"] = stack
context["url"] = self.get_redirect_url()
context["actions"] = table.render_row_actions(stack)
return context
@memoized.memoized_method
def get_data(self, request, **kwargs):
stack_id = kwargs['stack_id']
try:
stack = api.heat.stack_get(request, stack_id)
request.session['stack_id'] = stack.id
request.session['stack_name'] = stack.stack_name
return stack
except Exception:
msg = _("Unable to retrieve stack.")
exceptions.handle(request, msg, redirect=self.get_redirect_url())
@memoized.memoized_method
def get_template(self, request, **kwargs):
try:
stack_template = api.heat.template_get(
request,
kwargs['stack_id'])
return yaml.safe_dump(stack_template, indent=2)
except Exception:
msg = _("Unable to retrieve stack template.")
exceptions.handle(request, msg, redirect=self.get_redirect_url())
def get_tabs(self, request, **kwargs):
stack = self.get_data(request, **kwargs)
stack_template = self.get_template(request, **kwargs)
return self.tab_group_class(
request, stack=stack, stack_template=stack_template, **kwargs)
@staticmethod
def get_redirect_url():
return reverse('horizon:project:stacks:index')
class ResourceView(tabs.TabView):
tab_group_class = project_tabs.ResourceDetailTabs
template_name = 'horizon/common/_detail.html'
page_title = "{{ resource.resource_name|"\
"default:resource.logical_resource_id }}"
def get_context_data(self, **kwargs):
context = super(ResourceView, self).get_context_data(**kwargs)
context["resource"] = self.get_data(self.request, **kwargs)
context["metadata"] = self.get_metadata(self.request, **kwargs)
return context
@memoized.memoized_method
def get_data(self, request, **kwargs):
try:
resource = api.heat.resource_get(
request,
kwargs['stack_id'],
kwargs['resource_name'])
return resource
except Exception:
msg = _("Unable to retrieve resource.")
redirect = reverse('horizon:project:stacks:index')
exceptions.handle(request, msg, redirect=redirect)
@memoized.memoized_method
def get_metadata(self, request, **kwargs):
try:
metadata = api.heat.resource_metadata_get(
request,
kwargs['stack_id'],
kwargs['resource_name'])
return json.dumps(metadata, indent=2)
except Exception:
msg = _("Unable to retrieve metadata.")
redirect = reverse('horizon:project:stacks:index')
exceptions.handle(request, msg, redirect=redirect)
def get_tabs(self, request, **kwargs):
resource = self.get_data(request, **kwargs)
metadata = self.get_metadata(request, **kwargs)
return self.tab_group_class(
request, resource=resource, metadata=metadata, **kwargs)
class JSONView(django.views.generic.View):
def get(self, request, stack_id=''):
return HttpResponse(project_api.d3_data(request, stack_id=stack_id),
content_type="application/json")

View File

@ -1,8 +0,0 @@
from django.utils.translation import ugettext_lazy as _
# The slug of the panel group to be added to HORIZON_CONFIG. Required.
PANEL_GROUP = 'orchestration'
# The display name of the PANEL_GROUP. Required.
PANEL_GROUP_NAME = _('Orchestration')
# The slug of the dashboard the PANEL_GROUP associated with. Required.
PANEL_GROUP_DASHBOARD = 'project'

View File

@ -1,9 +0,0 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'stacks'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'orchestration'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.project.stacks.panel.Stacks'

View File

@ -1,10 +0,0 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'stacks.resource_types'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'orchestration'
# Python panel class of the PANEL to be added.
ADD_PANEL = ('openstack_dashboard.dashboards.project.'
'stacks.resource_types.panel.ResourceTypes')

View File

@ -1,10 +0,0 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'stacks.template_versions'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'orchestration'
# Python panel class of the PANEL to be added.
ADD_PANEL = ('openstack_dashboard.dashboards.project.'
'stacks.template_versions.panel.TemplateVersions')

View File

@ -18,7 +18,6 @@
from cinderclient import exceptions as cinderclient
from glanceclient.common import exceptions as glanceclient
from heatclient import exc as heatclient
from keystoneclient import exceptions as keystoneclient
from neutronclient.common import exceptions as neutronclient
from novaclient import exceptions as novaclient
@ -32,7 +31,6 @@ UNAUTHORIZED = (
novaclient.Unauthorized,
glanceclient.Unauthorized,
neutronclient.Unauthorized,
heatclient.HTTPUnauthorized,
)
@ -42,7 +40,6 @@ NOT_FOUND = (
novaclient.NotFound,
glanceclient.NotFound,
neutronclient.NotFound,
heatclient.HTTPNotFound,
)
@ -62,7 +59,5 @@ RECOVERABLE = (
neutronclient.Forbidden,
neutronclient.NeutronClientException,
swiftclient.ClientException,
heatclient.HTTPForbidden,
heatclient.HTTPException,
requests.RequestException,
)

View File

@ -502,7 +502,6 @@ TIME_ZONE = "UTC"
# 'compute': 'nova_policy.json',
# 'volume': 'cinder_policy.json',
# 'image': 'glance_policy.json',
# 'orchestration': 'heat_policy.json',
# 'network': 'neutron_policy.json',
#}
@ -608,11 +607,6 @@ LOGGING = {
'level': 'DEBUG',
'propagate': False,
},
'heatclient': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': False,
},
'swiftclient': {
'handlers': ['console'],
'level': 'DEBUG',

View File

@ -261,7 +261,6 @@ POLICY_FILES = {
'compute': 'nova_policy.json',
'volume': 'cinder_policy.json',
'image': 'glance_policy.json',
'orchestration': 'heat_policy.json',
'network': 'neutron_policy.json',
}

View File

@ -1,80 +0,0 @@
/*
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* 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.
*/
(function () {
'use strict';
angular
.module('horizon.app.core.openstack-service-api')
.factory('horizon.app.core.openstack-service-api.heat', heatAPI);
heatAPI.$inject = [
'horizon.framework.util.http.service',
'horizon.framework.widgets.toast.service'
];
/**
* @ngdoc service
* @name heatAPI
* @param {Object} apiService
* @param {Object} toastService
* @description Provides direct pass through to Heat with NO abstraction.
* @returns {Object} The service
*/
function heatAPI(apiService, toastService) {
var service = {
validate: validate,
getServices: getServices
};
return service;
/**
* @name validate
* @description
* Validate a template.
*
* @param {string} params
* - template_url
* Specifies the template to validate.
*
* @param {boolean} suppressError
* If passed in, this will not show the default error handling
* (horizon alert).
* @returns {Object} The result of the API call
*/
function validate(params, suppressError) {
var promise = apiService.post('/api/heat/validate/', params);
return suppressError ? promise : promise.error(function() {
toastService.add('error', gettext('Unable to validate the template.'));
});
}
/**
* @name getServices
* @description Get the list of heat services.
*
* @returns {Object} The listing result is an object with property "services." Each item is
* a service.
*/
function getServices() {
return apiService.get('/api/heat/services/')
.error(function () {
toastService.add('error', gettext('Unable to retrieve the heat services.'));
});
}
}
}());

View File

@ -1,80 +0,0 @@
/*
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* 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.
*/
(function() {
'use strict';
describe('Heat API', function() {
var testCall, service;
var apiService = {};
var toastService = {};
beforeEach(
module('horizon.mock.openstack-service-api',
function($provide, initServices) {
testCall = initServices($provide, apiService, toastService);
})
);
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(inject(['horizon.app.core.openstack-service-api.heat', function(heatAPI) {
service = heatAPI;
}]));
it('defines the service', function() {
expect(service).toBeDefined();
});
var tests = [
{
'func': 'validate',
'method': 'post',
'path': '/api/heat/validate/',
'data': {
'template_url':'http://localhost/test.template'
},
'error': 'Unable to validate the template.',
'testInput': [
{
'template_url':'http://localhost/test.template'
}
]
},
{
'func': 'getServices',
'method': 'get',
'path': '/api/heat/services/',
'error': 'Unable to retrieve the heat services.'
}
];
// Iterate through the defined tests and apply as Jasmine specs.
angular.forEach(tests, function(params) {
it('defines the ' + params.func + ' call properly', function() {
var callParams = [apiService, service, toastService, params];
testCall.apply(this, callParams);
});
});
it('suppresses the error for template validation as instructed by the param', function() {
spyOn(apiService, 'post').and.returnValue("promise");
expect(service.validate("whatever", true)).toBe("promise");
});
});
})();

View File

@ -1,69 +0,0 @@
# (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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 mock
from openstack_dashboard import api
from openstack_dashboard.api.rest import heat
from openstack_dashboard.test import helpers as test
class ValidateRestTestCase(test.TestCase):
@mock.patch.object(heat.api, 'heat')
def test_validate_post(self, hc):
body = '''{"template_url":"http://localhost/template.yaml"}'''
request = self.mock_rest_request(body=body)
hc.template_validate.return_value = ({'Description': 'foo'})
response = heat.Validate().post(request)
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {"Description": "foo"})
kwargs = json.loads(body)
hc.template_validate.assert_called_once_with(request, **kwargs)
class HeatRestTestCase(test.TestCase):
#
# Services
#
@test.create_stubs({api.base: ('is_service_enabled',)})
@mock.patch.object(heat.api, 'heat')
def test_services_get(self, hc):
request = self.mock_rest_request(GET={})
api.base.is_service_enabled(request, 'orchestration').AndReturn(True)
hc.service_list.return_value = [
mock.Mock(**{'to_dict.return_value': {'id': '1'}}),
mock.Mock(**{'to_dict.return_value': {'id': '2'}})
]
self.mox.ReplayAll()
response = heat.Services().get(request)
self.assertStatusCode(response, 200)
self.assertEqual(response.content.decode('utf-8'),
'{"items": [{"id": "1"}, {"id": "2"}]}')
hc.service_list.assert_called_once_with(request)
@test.create_stubs({api.base: ('is_service_enabled',)})
def test_services_get_disabled(self):
request = self.mock_rest_request(GET={})
api.base.is_service_enabled(request, 'orchestration').AndReturn(False)
self.mox.ReplayAll()
response = heat.Services().get(request)
self.assertStatusCode(response, 501)

View File

@ -1,358 +0,0 @@
# 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 six
from django.conf import settings
from django.test.utils import override_settings
from horizon import exceptions
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
class HeatApiTests(test.APITestCase):
def test_stack_list(self):
api_stacks = self.stacks.list()
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
heatclient = self.stub_heatclient()
heatclient.stacks = self.mox.CreateMockAnything()
heatclient.stacks.list(limit=limit,
sort_dir='desc',
sort_key='created_at',) \
.AndReturn(iter(api_stacks))
self.mox.ReplayAll()
stacks, has_more, has_prev = api.heat.stacks_list(self.request)
self.assertItemsEqual(stacks, api_stacks)
self.assertFalse(has_more)
self.assertFalse(has_prev)
@override_settings(API_RESULT_PAGE_SIZE=2)
def test_stack_list_sort_options(self):
# Verify that sort_dir and sort_key work
api_stacks = self.stacks.list()
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
sort_dir = 'asc'
sort_key = 'size'
heatclient = self.stub_heatclient()
heatclient.stacks = self.mox.CreateMockAnything()
heatclient.stacks.list(limit=limit,
sort_dir=sort_dir,
sort_key=sort_key,) \
.AndReturn(iter(api_stacks))
self.mox.ReplayAll()
stacks, has_more, has_prev = api.heat.stacks_list(self.request,
sort_dir=sort_dir,
sort_key=sort_key)
self.assertItemsEqual(stacks, api_stacks)
self.assertFalse(has_more)
self.assertFalse(has_prev)
@override_settings(API_RESULT_PAGE_SIZE=20)
def test_stack_list_pagination_less_page_size(self):
api_stacks = self.stacks.list()
page_size = settings.API_RESULT_PAGE_SIZE
sort_dir = 'desc'
sort_key = 'created_at'
heatclient = self.stub_heatclient()
heatclient.stacks = self.mox.CreateMockAnything()
heatclient.stacks.list(limit=page_size + 1,
sort_dir=sort_dir,
sort_key=sort_key,) \
.AndReturn(iter(api_stacks))
self.mox.ReplayAll()
stacks, has_more, has_prev = api.heat.stacks_list(self.request,
sort_dir=sort_dir,
sort_key=sort_key,
paginate=True)
expected_stacks = api_stacks[:page_size]
self.assertItemsEqual(stacks, expected_stacks)
self.assertFalse(has_more)
self.assertFalse(has_prev)
@override_settings(API_RESULT_PAGE_SIZE=10)
def test_stack_list_pagination_equal_page_size(self):
api_stacks = self.stacks.list()
page_size = settings.API_RESULT_PAGE_SIZE
sort_dir = 'desc'
sort_key = 'created_at'
heatclient = self.stub_heatclient()
heatclient.stacks = self.mox.CreateMockAnything()
heatclient.stacks.list(limit=page_size + 1,
sort_dir=sort_dir,
sort_key=sort_key,) \
.AndReturn(iter(api_stacks))
self.mox.ReplayAll()
stacks, has_more, has_prev = api.heat.stacks_list(self.request,
sort_dir=sort_dir,
sort_key=sort_key,
paginate=True)
expected_stacks = api_stacks[:page_size]
self.assertItemsEqual(stacks, expected_stacks)
self.assertFalse(has_more)
self.assertFalse(has_prev)
@override_settings(API_RESULT_PAGE_SIZE=2)
def test_stack_list_pagination_marker(self):
page_size = getattr(settings, 'API_RESULT_PAGE_SIZE', 20)
sort_dir = 'desc'
sort_key = 'created_at'
marker = 'nonsense'
api_stacks = self.stacks.list()
heatclient = self.stub_heatclient()
heatclient.stacks = self.mox.CreateMockAnything()
heatclient.stacks.list(limit=page_size + 1,
marker=marker,
sort_dir=sort_dir,
sort_key=sort_key,) \
.AndReturn(iter(api_stacks[:page_size + 1]))
self.mox.ReplayAll()
stacks, has_more, has_prev = api.heat.stacks_list(self.request,
marker=marker,
paginate=True,
sort_dir=sort_dir,
sort_key=sort_key,)
self.assertEqual(len(stacks), page_size)
self.assertItemsEqual(stacks, api_stacks[:page_size])
self.assertTrue(has_more)
self.assertTrue(has_prev)
@override_settings(API_RESULT_PAGE_SIZE=2)
def test_stack_list_pagination_marker_prev(self):
page_size = getattr(settings, 'API_RESULT_PAGE_SIZE', 20)
sort_dir = 'asc'
sort_key = 'created_at'
marker = 'nonsense'
api_stacks = self.stacks.list()
heatclient = self.stub_heatclient()
heatclient.stacks = self.mox.CreateMockAnything()
heatclient.stacks.list(limit=page_size + 1,
marker=marker,
sort_dir=sort_dir,
sort_key=sort_key,) \
.AndReturn(iter(api_stacks[:page_size + 1]))
self.mox.ReplayAll()
stacks, has_more, has_prev = api.heat.stacks_list(self.request,
marker=marker,
paginate=True,
sort_dir=sort_dir,
sort_key=sort_key,)
self.assertEqual(len(stacks), page_size)
self.assertItemsEqual(stacks, api_stacks[:page_size])
self.assertTrue(has_more)
self.assertTrue(has_prev)
def test_template_get(self):
api_stacks = self.stacks.list()
stack_id = api_stacks[0].id
mock_data_template = self.stack_templates.list()[0]
heatclient = self.stub_heatclient()
heatclient.stacks = self.mox.CreateMockAnything()
heatclient.stacks.template(stack_id).AndReturn(mock_data_template)
self.mox.ReplayAll()
template = api.heat.template_get(self.request, stack_id)
self.assertEqual(mock_data_template.data, template.data)
def test_stack_create(self):
api_stacks = self.stacks.list()
stack = api_stacks[0]
heatclient = self.stub_heatclient()
heatclient.stacks = self.mox.CreateMockAnything()
form_data = {'timeout_mins': 600}
password = 'secret'
heatclient.stacks.create(**form_data).AndReturn(stack)
self.mox.ReplayAll()
returned_stack = api.heat.stack_create(self.request,
password,
**form_data)
from heatclient.v1 import stacks
self.assertIsInstance(returned_stack, stacks.Stack)
def test_stack_update(self):
api_stacks = self.stacks.list()
stack = api_stacks[0]
stack_id = stack.id
heatclient = self.stub_heatclient()
heatclient.stacks = self.mox.CreateMockAnything()
form_data = {'timeout_mins': 600}
password = 'secret'
heatclient.stacks.update(stack_id, **form_data).AndReturn(stack)
self.mox.ReplayAll()
returned_stack = api.heat.stack_update(self.request,
stack_id,
password,
**form_data)
from heatclient.v1 import stacks
self.assertIsInstance(returned_stack, stacks.Stack)
def test_snapshot_create(self):
stack_id = self.stacks.first().id
snapshot_create = self.stack_snapshot_create.list()[0]
heatclient = self.stub_heatclient()
heatclient.stacks = self.mox.CreateMockAnything()
heatclient.stacks.snapshot(stack_id).AndReturn(snapshot_create)
self.mox.ReplayAll()
returned_snapshot_create_info = api.heat.snapshot_create(self.request,
stack_id)
self.assertEqual(returned_snapshot_create_info, snapshot_create)
def test_snapshot_list(self):
stack_id = self.stacks.first().id
snapshot_list = self.stack_snapshot.list()
heatclient = self.stub_heatclient()
heatclient.stacks = self.mox.CreateMockAnything()
heatclient.stacks.snapshot_list(stack_id).AndReturn(snapshot_list)
self.mox.ReplayAll()
returned_snapshots = api.heat.snapshot_list(self.request, stack_id)
self.assertItemsEqual(returned_snapshots, snapshot_list)
def test_get_template_files_with_template_data(self):
tmpl = '''
# comment
heat_template_version: 2013-05-23
resources:
server1:
type: OS::Nova::Server
properties:
flavor: m1.medium
image: cirros
'''
expected_files = {}
files = api.heat.get_template_files(template_data=tmpl)[0]
self.assertEqual(files, expected_files)
def test_get_template_files(self):
tmpl = '''
# comment
heat_template_version: 2013-05-23
resources:
server1:
type: OS::Nova::Server
properties:
flavor: m1.medium
image: cirros
user_data_format: RAW
user_data:
get_file: http://test.example/example
'''
expected_files = {u'http://test.example/example': b'echo "test"'}
url = 'http://test.example/example'
data = b'echo "test"'
self.mox.StubOutWithMock(six.moves.urllib.request, 'urlopen')
six.moves.urllib.request.urlopen(url).AndReturn(
six.BytesIO(data))
self.mox.ReplayAll()
files = api.heat.get_template_files(template_data=tmpl)[0]
self.assertEqual(files, expected_files)
def test_get_template_files_with_template_url(self):
url = 'https://test.example/example.yaml'
data = b'''
# comment
heat_template_version: 2013-05-23
resources:
server1:
type: OS::Nova::Server
properties:
flavor: m1.medium
image: cirros
user_data_format: RAW
user_data:
get_file: http://test.example/example
'''
url2 = 'http://test.example/example'
data2 = b'echo "test"'
expected_files = {'http://test.example/example': b'echo "test"'}
self.mox.StubOutWithMock(six.moves.urllib.request, 'urlopen')
six.moves.urllib.request.urlopen(url).AndReturn(
six.BytesIO(data))
six.moves.urllib.request.urlopen(url2).AndReturn(
six.BytesIO(data2))
self.mox.ReplayAll()
files = api.heat.get_template_files(template_url=url)[0]
self.assertEqual(files, expected_files)
def test_get_template_files_invalid(self):
tmpl = '''
# comment
heat_template_version: 2013-05-23
resources:
server1:
type: OS::Nova::Server
properties:
flavor: m1.medium
image: cirros
user_data_format: RAW
user_data:
get_file: file:///example
'''
try:
api.heat.get_template_files(template_data=tmpl)[0]
except exceptions.GetFileError:
self.assertRaises(exceptions.GetFileError)
def test_template_version_list(self):
api_template_versions = self.template_versions.list()
heatclient = self.stub_heatclient()
heatclient.template_versions = self.mox.CreateMockAnything()
heatclient.template_versions.list().AndReturn(api_template_versions)
self.mox.ReplayAll()
template_versions = api.heat.template_version_list(self.request)
self.assertItemsEqual(template_versions, api_template_versions)
def test_template_function_list(self):
template_version = self.template_versions.first().version
api_template_functions = self.template_functions.list()
heatclient = self.stub_heatclient()
heatclient.template_versions = self.mox.CreateMockAnything()
heatclient.template_versions.get(
template_version).AndReturn(api_template_functions)
self.mox.ReplayAll()
template_functions = api.heat.template_function_list(
self.request, template_version)
self.assertItemsEqual(template_functions, api_template_functions)

View File

@ -37,7 +37,6 @@ from django.utils import http
from cinderclient import client as cinder_client
import glanceclient
from heatclient import client as heat_client
from keystoneclient.v2_0 import client as keystone_client
import mock
from mox3 import mox
@ -433,7 +432,6 @@ class APITestCase(TestCase):
self._original_novaclient = api.nova.novaclient
self._original_neutronclient = api.neutron.neutronclient
self._original_cinderclient = api.cinder.cinderclient
self._original_heatclient = api.heat.heatclient
# Replace the clients with our stubs.
api.glance.glanceclient = fake_glanceclient
@ -441,8 +439,6 @@ class APITestCase(TestCase):
api.nova.novaclient = fake_novaclient
api.neutron.neutronclient = lambda request: self.stub_neutronclient()
api.cinder.cinderclient = lambda request: self.stub_cinderclient()
api.heat.heatclient = (lambda request, password=None:
self.stub_heatclient())
def tearDown(self):
super(APITestCase, self).tearDown()
@ -451,7 +447,6 @@ class APITestCase(TestCase):
api.keystone.keystoneclient = self._original_keystoneclient
api.neutron.neutronclient = self._original_neutronclient
api.cinder.cinderclient = self._original_cinderclient
api.heat.heatclient = self._original_heatclient
def stub_novaclient(self):
if not hasattr(self, "novaclient"):
@ -518,12 +513,6 @@ class APITestCase(TestCase):
expected_calls -= 1
return self.swiftclient
def stub_heatclient(self):
if not hasattr(self, "heatclient"):
self.mox.StubOutWithMock(heat_client, 'Client')
self.heatclient = self.mox.CreateMock(heat_client.Client)
return self.heatclient
class APIMockTestCase(APITestCase):
def stub_cinderclient(self):

View File

@ -80,8 +80,6 @@ NetworkGroup = [
AvailableServiceGroup = [
cfg.BoolOpt('neutron',
default=True),
cfg.BoolOpt('heat',
default=True),
]
SeleniumGroup = [

View File

@ -77,8 +77,6 @@ tenant_network_cidr=10.100.0.0/16
[service_available]
# Whether is Neutron expected to be available (boolean value)
neutron=True
# Whether is Heat expected to be available (boolean value)
heat=True
[scenario]
# ssh username for image file (string value)

View File

@ -106,13 +106,6 @@ class Navigation(object):
"Containers",
)
},
"Orchestration":
{
ITEMS:
(
"Stacks",
)
}
},
"Admin":
{

View File

@ -1,99 +0,0 @@
# 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 openstack_dashboard.test.integration_tests import config
from openstack_dashboard.test.integration_tests.pages import basepage
from openstack_dashboard.test.integration_tests.regions import forms
from openstack_dashboard.test.integration_tests.regions import tables
class StacksTable(tables.TableRegion):
name = "stacks"
SELECT_TEMPLATE_FORM_FIELDS = ("template_source", "template_upload",
"template_data", "template_url",
"environment_source", "environment_upload",
"environment_data")
LAUNCH_STACK_FORM_FIELDS = ("stack_name", "timeout_mins",
"enable_rollback", "password")
@tables.bind_table_action('launch')
def select_template(self, launch_button):
launch_button.click()
return forms.FormRegion(
self.driver, self.conf,
field_mappings=self.SELECT_TEMPLATE_FORM_FIELDS)
def launch_stack(self):
return forms.FormRegion(self.driver, self.conf,
field_mappings=self.LAUNCH_STACK_FORM_FIELDS)
@tables.bind_table_action('delete')
def delete_stack(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
class StacksPage(basepage.BaseNavigationPage):
DEFAULT_TEMPLATE_SOURCE = 'raw'
CONFIG = config.get_config()
DEFAULT_PASSWORD = CONFIG.identity.admin_password
STACKS_TABLE_NAME_COLUMN = 'name'
STACKS_TABLE_STATUS_COLUMN = 'stack_status'
def __init__(self, driver, conf):
super(StacksPage, self).__init__(driver, conf)
self._page_title = "Stacks"
@property
def stacks_table(self):
return StacksTable(self.driver, self.conf)
def _get_row_with_stack_name(self, name):
return self.stacks_table.get_row(self.STACKS_TABLE_NAME_COLUMN, name)
def create_stack(self, stack_name, template_data,
template_source=DEFAULT_TEMPLATE_SOURCE,
environment_source=None,
environment_upload=None,
timeout_mins=None,
enable_rollback=None,
password=DEFAULT_PASSWORD):
select_template_form = self.stacks_table.select_template()
select_template_form.template_source.value = template_source
select_template_form.template_data.text = template_data
select_template_form.submit()
launch_stack_form = self.stacks_table.launch_stack()
launch_stack_form.stack_name.text = stack_name
launch_stack_form.password.text = password
launch_stack_form.submit()
def delete_stack(self, name):
row = self._get_row_with_stack_name(name)
row.mark()
confirm_delete_stacks_form = self.stacks_table.delete_stack()
confirm_delete_stacks_form.submit()
def is_stack_present(self, name):
return bool(self._get_row_with_stack_name(name))
def is_stack_create_complete(self, name):
def cell_getter():
row = self._get_row_with_stack_name(name)
return row and row.cells[self.STACKS_TABLE_STATUS_COLUMN]
return bool(self.stacks_table.wait_cell_status(cell_getter,
'Create Complete'))
def is_stack_deleted(self, name):
return self.stacks_table.is_row_deleted(
lambda: self._get_row_with_stack_name(name))

View File

@ -1,73 +0,0 @@
# 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 os
from openstack_dashboard.test.integration_tests import decorators
from openstack_dashboard.test.integration_tests import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestStacks(helpers.AdminTestCase):
KEYPAIR_NAME = 'keypair_for_stack'
STACKS_NAME = helpers.gen_random_resource_name('stack', timestamp=False)
STACK_TEMPLATE_PATH = os.path.join(
os.path.dirname(__file__), 'test-data/stack_template')
def setUp(self):
super(TestStacks, self).setUp()
keypair_page = self.home_pg.\
go_to_compute_accessandsecurity_keypairspage()
keypair_page.create_keypair(self.KEYPAIR_NAME)
keypair_page = self.home_pg.\
go_to_compute_accessandsecurity_keypairspage()
self.assertTrue(keypair_page.is_keypair_present(self.KEYPAIR_NAME))
def cleanup():
keypair_page = self.home_pg.\
go_to_compute_accessandsecurity_keypairspage()
keypair_page.delete_keypairs(self.KEYPAIR_NAME)
keypair_page.find_message_and_dismiss(messages.SUCCESS)
self.addCleanup(cleanup)
@decorators.skip_because(bugs=['1584057'])
@decorators.services_required("heat")
def test_create_delete_stack(self):
"""tests the stack creation and deletion functionality
* creates a new stack
* verifies the stack appears in the stacks table in Create Complete
state
* deletes the newly created stack
* verifies the stack does not appear in the table after deletion
"""
with open(self.STACK_TEMPLATE_PATH, 'r') as f:
template = f.read()
input_template = template.format(self.KEYPAIR_NAME,
self.CONFIG.image.images_list[0],
"public")
stacks_page = self.home_pg.go_to_orchestration_stackspage()
stacks_page.create_stack(self.STACKS_NAME, input_template)
self.assertTrue(
stacks_page.find_message_and_dismiss(messages.INFO))
self.assertFalse(
stacks_page.find_message_and_dismiss(messages.ERROR))
self.assertTrue(stacks_page.is_stack_present(self.STACKS_NAME))
self.assertTrue(stacks_page.is_stack_create_complete(self.STACKS_NAME))
stacks_page.delete_stack(self.STACKS_NAME)
self.assertTrue(
stacks_page.find_message_and_dismiss(messages.SUCCESS))
self.assertFalse(
stacks_page.find_message_and_dismiss(messages.ERROR))
self.assertTrue(stacks_page.is_stack_deleted(self.STACKS_NAME))

View File

@ -14,7 +14,6 @@
from cinderclient import exceptions as cinder_exceptions
import glanceclient.exc as glance_exceptions
import heatclient.exc as heat_exceptions
from keystoneclient import exceptions as keystone_exceptions
from neutronclient.common import exceptions as neutron_exceptions
from novaclient import exceptions as nova_exceptions
@ -81,6 +80,3 @@ def data(TEST):
cinder_exception = cinder_exceptions.BadRequest
TEST.exceptions.cinder = create_stubbed_exception(cinder_exception)
heat_exception = heat_exceptions.HTTPException
TEST.exceptions.heat = create_stubbed_exception(heat_exception)

View File

@ -1,617 +0,0 @@
# 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 heatclient.v1 import resource_types
from heatclient.v1 import resources
from heatclient.v1 import services
from heatclient.v1 import stacks
from heatclient.v1 import template_versions
from openstack_dashboard.test.test_data import utils
# A slightly hacked up copy of a sample cloudformation template for testing.
TEMPLATE = """
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "AWS CloudFormation Sample Template.",
"Parameters": {
"KeyName": {
"Description": "Name of an EC2 Key Pair to enable SSH access to the instances",
"Type": "String"
},
"InstanceType": {
"Description": "WebServer EC2 instance type",
"Type": "String",
"Default": "m1.small",
"AllowedValues": [
"m1.tiny",
"m1.small",
"m1.medium",
"m1.large",
"m1.xlarge"
],
"ConstraintDescription": "must be a valid EC2 instance type."
},
"DBName": {
"Default": "wordpress",
"Description": "The WordPress database name",
"Type": "String",
"MinLength": "1",
"MaxLength": "64",
"AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*",
"ConstraintDescription": "must begin with a letter and..."
},
"DBUsername": {
"Default": "admin",
"NoEcho": "true",
"Description": "The WordPress database admin account username",
"Type": "String",
"MinLength": "1",
"MaxLength": "16",
"AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*",
"ConstraintDescription": "must begin with a letter and..."
},
"DBPassword": {
"Default": "admin",
"NoEcho": "true",
"Description": "The WordPress database admin account password",
"Type": "String",
"MinLength": "1",
"MaxLength": "41",
"AllowedPattern": "[a-zA-Z0-9]*",
"ConstraintDescription": "must contain only alphanumeric characters."
},
"DBRootPassword": {
"Default": "admin",
"NoEcho": "true",
"Description": "Root password for MySQL",
"Type": "String",
"MinLength": "1",
"MaxLength": "41",
"AllowedPattern": "[a-zA-Z0-9]*",
"ConstraintDescription": "must contain only alphanumeric characters."
},
"LinuxDistribution": {
"Default": "F17",
"Description": "Distribution of choice",
"Type": "String",
"AllowedValues": [
"F18",
"F17",
"U10",
"RHEL-6.1",
"RHEL-6.2",
"RHEL-6.3"
]
},
"Network": {
"Type": "String",
"CustomConstraint": "neutron.network"
}
},
"Mappings": {
"AWSInstanceType2Arch": {
"m1.tiny": {
"Arch": "32"
},
"m1.small": {
"Arch": "64"
},
"m1.medium": {
"Arch": "64"
},
"m1.large": {
"Arch": "64"
},
"m1.xlarge": {
"Arch": "64"
}
},
"DistroArch2AMI": {
"F18": {
"32": "F18-i386-cfntools",
"64": "F18-x86_64-cfntools"
},
"F17": {
"32": "F17-i386-cfntools",
"64": "F17-x86_64-cfntools"
},
"U10": {
"32": "U10-i386-cfntools",
"64": "U10-x86_64-cfntools"
},
"RHEL-6.1": {
"32": "rhel61-i386-cfntools",
"64": "rhel61-x86_64-cfntools"
},
"RHEL-6.2": {
"32": "rhel62-i386-cfntools",
"64": "rhel62-x86_64-cfntools"
},
"RHEL-6.3": {
"32": "rhel63-i386-cfntools",
"64": "rhel63-x86_64-cfntools"
}
}
},
"Resources": {
"WikiDatabase": {
"Type": "AWS::EC2::Instance",
"Metadata": {
"AWS::CloudFormation::Init": {
"config": {
"packages": {
"yum": {
"mysql": [],
"mysql-server": [],
"httpd": [],
"wordpress": []
}
},
"services": {
"systemd": {
"mysqld": {
"enabled": "true",
"ensureRunning": "true"
},
"httpd": {
"enabled": "true",
"ensureRunning": "true"
}
}
}
}
}
},
"Properties": {
"ImageId": {
"Fn::FindInMap": [
"DistroArch2AMI",
{
"Ref": "LinuxDistribution"
},
{
"Fn::FindInMap": [
"AWSInstanceType2Arch",
{
"Ref": "InstanceType"
},
"Arch"
]
}
]
},
"InstanceType": {
"Ref": "InstanceType"
},
"KeyName": {
"Ref": "KeyName"
},
"UserData": {
"Fn::Base64": {
"Fn::Join": [
"",
[
"#!/bin/bash -v\\n",
"/opt/aws/bin/cfn-init\\n"
]
]
}
}
}
}
},
"Outputs": {
"WebsiteURL": {
"Value": {
"Fn::Join": [
"",
[
"http://",
{
"Fn::GetAtt": [
"WikiDatabase",
"PublicIp"
]
},
"/wordpress"
]
]
},
"Description": "URL for Wordpress wiki"
}
}
}
"""
VALIDATE = """
{
"Description": "AWS CloudFormation Sample Template.",
"Parameters": {
"DBUsername": {
"Type": "String",
"Description": "The WordPress database admin account username",
"Default": "admin",
"MinLength": "1",
"AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*",
"NoEcho": "true",
"MaxLength": "16",
"ConstraintDescription": "must begin with a letter and..."
},
"LinuxDistribution": {
"Default": "F17",
"Type": "String",
"Description": "Distribution of choice",
"AllowedValues": [
"F18",
"F17",
"U10",
"RHEL-6.1",
"RHEL-6.2",
"RHEL-6.3"
]
},
"DBRootPassword": {
"Type": "String",
"Description": "Root password for MySQL",
"Default": "admin",
"MinLength": "1",
"AllowedPattern": "[a-zA-Z0-9]*",
"NoEcho": "true",
"MaxLength": "41",
"ConstraintDescription": "must contain only alphanumeric characters."
},
"KeyName": {
"Type": "String",
"Description": "Name of an EC2 Key Pair to enable SSH access to the instances"
},
"DBName": {
"Type": "String",
"Description": "The WordPress database name",
"Default": "wordpress",
"MinLength": "1",
"AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*",
"MaxLength": "64",
"ConstraintDescription": "must begin with a letter and..."
},
"DBPassword": {
"Type": "String",
"Description": "The WordPress database admin account password",
"Default": "admin",
"MinLength": "1",
"AllowedPattern": "[a-zA-Z0-9]*",
"NoEcho": "true",
"MaxLength": "41",
"ConstraintDescription": "must contain only alphanumeric characters."
},
"InstanceType": {
"Default": "m1.small",
"Type": "String",
"ConstraintDescription": "must be a valid EC2 instance type.",
"Description": "WebServer EC2 instance type",
"AllowedValues": [
"m1.tiny",
"m1.small",
"m1.medium",
"m1.large",
"m1.xlarge"
]
},
"Network": {
"Type": "String",
"CustomConstraint": "neutron.network"
}
}
}
"""
ENVIRONMENT = """
parameters:
InstanceType: m1.xsmall
db_password: verybadpass
KeyName: heat_key
"""
SNAPSHOT_CREATE = """
{
"status": "IN_PROGRESS",
"name": "None",
"data": "None",
"creation_time": "2016-02-19T07:25:23.494152",
"status_reason": "None",
"id": "8af90c07-b788-44ee-a8ab-5990197f5e32"
}
"""
class Environment(object):
def __init__(self, data):
self.data = data
class Template(object):
def __init__(self, data, validate):
self.data = data
self.validate = validate
class Snapshot(object):
def __init__(self, data):
self.data = data
def data(TEST):
TEST.stacks = utils.TestDataContainer()
TEST.stack_templates = utils.TestDataContainer()
TEST.stack_environments = utils.TestDataContainer()
TEST.stack_snapshot_create = utils.TestDataContainer()
TEST.stack_snapshot = utils.TestDataContainer()
TEST.resource_types = utils.TestDataContainer()
TEST.heat_resources = utils.TestDataContainer()
TEST.heat_services = utils.TestDataContainer()
TEST.template_versions = utils.TestDataContainer()
TEST.template_functions = utils.TestDataContainer()
# Services
service_1 = services.Service(services.ServiceManager(None), {
"status": "up",
"binary": "heat-engine",
"report_interval": 60,
"engine_id": "2f7b5a9b-c50b-4b01-8248-f89f5fb338d1",
"created_at": "2015-02-06T03:23:32.000000",
"hostname": "mrkanag",
"updated_at": "2015-02-20T09:49:52.000000",
"topic": "engine",
"host": "engine-1",
"deleted_at": None,
"id": "1efd7015-5016-4caa-b5c8-12438af7b100"
})
service_2 = services.Service(services.ServiceManager(None), {
"status": "up",
"binary": "heat-engine",
"report_interval": 60,
"engine_id": "2f7b5a9b-c50b-4b01-8248-f89f5fb338d2",
"created_at": "2015-02-06T03:23:32.000000",
"hostname": "mrkanag",
"updated_at": "2015-02-20T09:49:52.000000",
"topic": "engine",
"host": "engine-2",
"deleted_at": None,
"id": "1efd7015-5016-4caa-b5c8-12438af7b100"
})
TEST.heat_services.add(service_1)
TEST.heat_services.add(service_2)
# Data return by heatclient.
TEST.api_resource_types = utils.TestDataContainer()
for i in range(10):
stack_data = {
"description": "No description",
"links": [{
"href": "http://192.168.1.70:8004/v1/"
"051c727ee67040d6a7b7812708485a97/"
"stacks/stack-test{0}/"
"05b4f39f-ea96-4d91-910c-e758c078a089{0}".format(i),
"rel": "self"
}],
"parameters": {
'DBUsername': '******',
'InstanceType': 'm1.small',
'AWS::StackId': (
'arn:openstack:heat::2ce287:stacks/teststack/88553ec'),
'DBRootPassword': '******',
'AWS::StackName': "teststack{0}".format(i),
'DBPassword': '******',
'AWS::Region': 'ap-southeast-1',
'DBName': u'wordpress'
},
"stack_status_reason": "Stack successfully created",
"stack_name": "stack-test{0}".format(i),
"creation_time": "2013-04-22T00:11:39Z",
"updated_time": "2013-04-22T00:11:39Z",
"stack_status": "CREATE_COMPLETE",
"id": "05b4f39f-ea96-4d91-910c-e758c078a089{0}".format(i)
}
stack = stacks.Stack(stacks.StackManager(None), stack_data)
TEST.stacks.add(stack)
for i in range(10):
snapshot_data = {
"status": "COMPLETE",
"name": 'null',
"data": {
"files": {},
"status": "COMPLETE",
"name": "zhao3",
"tags": ["a", " 123", " b", " 456"],
"stack_user_project_id": "3cba4460875444049a2a7cc5420ccddb",
"environment": {
"encrypted_param_names": [],
"parameter_defaults": {},
"event_sinks": [],
"parameters": {},
"resource_registry": {
"resources": {}
}
},
"template": {
"heat_template_version": "2013-05-23",
"description":
"HOT template for Test.",
"resources": {
"private_subnet": {
"type": "OS::Neutron::Subnet",
"properties": {
"network_id": {"get_resource": "private_net"},
"cidr": "172.16.2.0/24",
"gateway_ip": "172.16.2.1"
}
},
"private_net": {
"type": "OS::Neutron::Net",
"properties": {"name": "private-net"}
}
}
},
"action": "SNAPSHOT",
"project_id": "1acd0026829f4d28bb2eff912d7aad0d",
"id": "70650725-bdbd-419f-b53f-5707767bfe0e",
"resources": {
"private_subnet": {
"status": "COMPLETE",
"name": "private_subnet",
"resource_data": {},
"resource_id": "9c7211b3-31c7-41f6-b92a-442ad3f71ef0",
"action": "SNAPSHOT",
"type": "OS::Neutron::Subnet",
"metadata": {}
},
"private_net": {
"status": "COMPLETE",
"name": "private_net",
"resource_data": {},
"resource_id": "ff4fd287-31b2-4d00-bc96-c409bc1db027",
"action": "SNAPSHOT",
"type": "OS::Neutron::Net",
"metadata": {}
}
}
},
"creation_time": "2016-02-21T04:02:54",
"status_reason": "Stack SNAPSHOT completed successfully",
"id": "01558a3b-ba05-4427-bbb4-1e4ab71cfca{0}".format(i)
}
TEST.stack_snapshot.add(snapshot_data)
TEST.stack_templates.add(Template(TEMPLATE, VALIDATE))
TEST.stack_environments.add(Environment(ENVIRONMENT))
TEST.stack_snapshot_create.add(Snapshot(SNAPSHOT_CREATE))
# Resource types list
r_type_1 = {
"resource_type": "AWS::CloudFormation::Stack",
"attributes": {},
"properties": {
"Parameters": {
"description":
"The set of parameters passed to this nested stack.",
"immutable": False,
"required": False,
"type": "map",
"update_allowed": True},
"TemplateURL": {
"description": "The URL of a template that specifies"
" the stack to be created as a resource.",
"immutable": False,
"required": True,
"type": "string",
"update_allowed": True},
"TimeoutInMinutes": {
"description": "The length of time, in minutes,"
" to wait for the nested stack creation.",
"immutable": False,
"required": False,
"type": "number",
"update_allowed": True}
}
}
r_type_2 = {
"resource_type": "OS::Heat::CloudConfig",
"attributes": {
"config": {
"description": "The config value of the software config."}
},
"properties": {
"cloud_config": {
"description": "Map representing the cloud-config data"
" structure which will be formatted as YAML.",
"immutable": False,
"required": False,
"type": "map",
"update_allowed": False}
}
}
r_types_list = [r_type_1, r_type_2]
for rt in r_types_list:
r_type = resource_types.ResourceType(
resource_types.ResourceTypeManager(None), rt['resource_type'])
TEST.resource_types.add(r_type)
TEST.api_resource_types.add(rt)
# Resources
resource_1 = resources.Resource(resources.ResourceManager(None), {
"logical_resource_id": "my_resource",
"physical_resource_id": "7b5e29b1-c94d-402d-b69c-df9ac6dfc0ce",
"resource_name": "my_resource",
"links": [
{
"href": "http://192.168.1.70:8004/v1/"
"051c727ee67040d6a7b7812708485a97/"
"stacks/%s/%s/resources/my_resource" %
(TEST.stacks.first().stack_name,
TEST.stacks.first().id),
"rel": "self"
},
{
"href": "http://192.168.1.70:8004/v1/"
"051c727ee67040d6a7b7812708485a97/"
"stacks/%s/%s" %
(TEST.stacks.first().stack_name,
TEST.stacks.first().id),
"rel": "stack"
}
],
"attributes": {
"metadata": {}
}
})
TEST.heat_resources.add(resource_1)
# Template versions
template_version_1 = template_versions.TemplateVersion(
template_versions.TemplateVersionManager(None), {
"version": "HeatTemplateFormatVersion.2012-12-12",
"type": "cfn"
})
template_version_2 = template_versions.TemplateVersion(
template_versions.TemplateVersionManager(None), {
"version": "heat_template_version.2013-05-23",
"type": "hot"
})
TEST.template_versions.add(template_version_1)
TEST.template_versions.add(template_version_2)
# Template functions
template_function_1 = template_versions.TemplateVersion(
template_versions.TemplateVersionManager(None), {
"functions": "Fn::GetAZs",
"description": "A function for retrieving the availability zones."
})
template_function_2 = template_versions.TemplateVersion(
template_versions.TemplateVersionManager(None), {
"functions": "Fn::Join",
"description": "A function for joining strings."
})
TEST.template_functions.add(template_function_1)
TEST.template_functions.add(template_function_2)

View File

@ -103,14 +103,6 @@ SERVICE_CATALOG = [
"adminURL": "http://admin.nova.example.com:8773/services/Admin",
"publicURL": "http://public.nova.example.com:8773/services/Cloud",
"internalURL": "http://int.nova.example.com:8773/services/Cloud"}]},
{"type": "orchestration",
"name": "Heat",
"endpoints_links": [],
"endpoints": [
{"region": "RegionOne",
"adminURL": "http://admin.heat.example.com:8004/v1",
"publicURL": "http://public.heat.example.com:8004/v1",
"internalURL": "http://int.heat.example.com:8004/v1"}]}
]

View File

@ -17,7 +17,6 @@ def load_test_data(load_onto=None):
from openstack_dashboard.test.test_data import cinder_data
from openstack_dashboard.test.test_data import exceptions
from openstack_dashboard.test.test_data import glance_data
from openstack_dashboard.test.test_data import heat_data
from openstack_dashboard.test.test_data import keystone_data
from openstack_dashboard.test.test_data import neutron_data
from openstack_dashboard.test.test_data import nova_data
@ -32,7 +31,6 @@ def load_test_data(load_onto=None):
cinder_data.data,
neutron_data.data,
swift_data.data,
heat_data.data,
)
if load_onto:
for data_func in loaders:

View File

@ -0,0 +1,11 @@
---
upgrade:
- |
Heat dashboard is now split out into a separate project
``heat-dashboard``. All new features and maintenances are
provided from the new project from now on. The new project provides
all features available in Horizon in the past release.
To continue to use heat dashboard, install ``heat-dashboard``
and set up the horizon plugin configuration file in ``enabled`` directory.
For more information, see ``heat-dashboard`` documentation
https://docs.openstack.org/heat-dashboard/latest/.

View File

@ -30,7 +30,6 @@ pymongo!=3.1,>=3.0.2 # Apache-2.0
pyScss!=1.3.5,>=1.3.4 # MIT License
python-cinderclient>=3.2.0 # Apache-2.0
python-glanceclient>=2.8.0 # Apache-2.0
python-heatclient>=1.10.0 # Apache-2.0
python-keystoneclient>=3.8.0 # Apache-2.0
python-neutronclient>=6.3.0 # Apache-2.0
python-novaclient>=9.1.0 # Apache-2.0

View File

@ -1,4 +1,4 @@
# This file contains various customized Devstack settings that Horizon uses at
# gate for integration tests job
export ENABLED_SERVICES=heat,h-eng,h-api,h-api-cfn,h-api-cw