summaryrefslogtreecommitdiff
path: root/openstack_dashboard/dashboards/project
diff options
context:
space:
mode:
Diffstat (limited to 'openstack_dashboard/dashboards/project')
-rw-r--r--openstack_dashboard/dashboards/project/stacks/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/project/stacks/api.py83
-rw-r--r--openstack_dashboard/dashboards/project/stacks/forms.py488
-rw-r--r--openstack_dashboard/dashboards/project/stacks/mappings.py350
-rw-r--r--openstack_dashboard/dashboards/project/stacks/panel.py21
-rw-r--r--openstack_dashboard/dashboards/project/stacks/resource_types/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/project/stacks/resource_types/panel.py23
-rw-r--r--openstack_dashboard/dashboards/project/stacks/resource_types/tables.py36
-rw-r--r--openstack_dashboard/dashboards/project/stacks/resource_types/tabs.py32
-rw-r--r--openstack_dashboard/dashboards/project/stacks/resource_types/templates/stacks.resource_types/_details.html15
-rw-r--r--openstack_dashboard/dashboards/project/stacks/resource_types/tests.py52
-rw-r--r--openstack_dashboard/dashboards/project/stacks/resource_types/urls.py22
-rw-r--r--openstack_dashboard/dashboards/project/stacks/resource_types/views.py78
-rw-r--r--openstack_dashboard/dashboards/project/stacks/sro.py44
-rw-r--r--openstack_dashboard/dashboards/project/stacks/tables.py413
-rw-r--r--openstack_dashboard/dashboards/project/stacks/tabs.py173
-rw-r--r--openstack_dashboard/dashboards/project/stacks/template_versions/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/project/stacks/template_versions/panel.py23
-rw-r--r--openstack_dashboard/dashboards/project/stacks/template_versions/tables.py52
-rw-r--r--openstack_dashboard/dashboards/project/stacks/template_versions/tabs.py51
-rw-r--r--openstack_dashboard/dashboards/project/stacks/template_versions/templates/stacks.template_versions/_details.html3
-rw-r--r--openstack_dashboard/dashboards/project/stacks/template_versions/templates/stacks.template_versions/index.html7
-rw-r--r--openstack_dashboard/dashboards/project/stacks/template_versions/tests.py79
-rw-r--r--openstack_dashboard/dashboards/project/stacks/template_versions/urls.py24
-rw-r--r--openstack_dashboard/dashboards/project/stacks/template_versions/views.py61
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_change_template.html7
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_create.html6
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_events.html3
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_overview.html55
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_resources.html3
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_topology.html9
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview.html6
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_details.html58
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_template.html7
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_resource_info.html10
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_resource_overview.html38
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_select_template.html7
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_stack_info.html14
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_stack_template.html5
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/_update.html6
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/change_template.html7
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/create.html7
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/preview.html7
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_details.html7
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_template.html7
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/select_template.html7
-rw-r--r--openstack_dashboard/dashboards/project/stacks/templates/stacks/update.html7
-rw-r--r--openstack_dashboard/dashboards/project/stacks/tests.py1003
-rw-r--r--openstack_dashboard/dashboards/project/stacks/urls.py38
-rw-r--r--openstack_dashboard/dashboards/project/stacks/views.py358
50 files changed, 0 insertions, 3812 deletions
diff --git a/openstack_dashboard/dashboards/project/stacks/__init__.py b/openstack_dashboard/dashboards/project/stacks/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/openstack_dashboard/dashboards/project/stacks/__init__.py
+++ /dev/null
diff --git a/openstack_dashboard/dashboards/project/stacks/api.py b/openstack_dashboard/dashboards/project/stacks/api.py
deleted file mode 100644
index ce5affd..0000000
--- a/openstack_dashboard/dashboards/project/stacks/api.py
+++ /dev/null
@@ -1,83 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import json
14
15from openstack_dashboard.api import heat
16
17from openstack_dashboard.dashboards.project.stacks import mappings
18from openstack_dashboard.dashboards.project.stacks import sro
19
20
21class Stack(object):
22 pass
23
24
25def d3_data(request, stack_id=''):
26 try:
27 stack = heat.stack_get(request, stack_id)
28 except Exception:
29 stack = Stack()
30 stack.id = stack_id
31 stack.stack_name = request.session.get('stack_name', '')
32 stack.stack_status = 'DELETE_COMPLETE'
33 stack.stack_status_reason = 'DELETE_COMPLETE'
34
35 try:
36 resources = heat.resources_list(request, stack.stack_name)
37 except Exception:
38 resources = []
39
40 d3_data = {"nodes": [], "stack": {}}
41 if stack:
42 stack_image = mappings.get_resource_image(stack.stack_status, 'stack')
43 stack_node = {
44 'stack_id': stack.id,
45 'name': stack.stack_name,
46 'status': stack.stack_status,
47 'image': stack_image,
48 'image_size': 60,
49 'image_x': -30,
50 'image_y': -30,
51 'text_x': 40,
52 'text_y': ".35em",
53 'in_progress': (stack.status == 'IN_PROGRESS'),
54 'info_box': sro.stack_info(stack, stack_image)
55 }
56 d3_data['stack'] = stack_node
57
58 if resources:
59 for resource in resources:
60 resource_image = mappings.get_resource_image(
61 resource.resource_status,
62 resource.resource_type)
63 resource_status = mappings.get_resource_status(
64 resource.resource_status)
65 if resource_status in ('IN_PROGRESS', 'INIT'):
66 in_progress = True
67 else:
68 in_progress = False
69 resource_node = {
70 'name': resource.resource_name,
71 'status': resource.resource_status,
72 'image': resource_image,
73 'required_by': resource.required_by,
74 'image_size': 50,
75 'image_x': -25,
76 'image_y': -25,
77 'text_x': 35,
78 'text_y': ".35em",
79 'in_progress': in_progress,
80 'info_box': sro.resource_info(resource)
81 }
82 d3_data['nodes'].append(resource_node)
83 return json.dumps(d3_data)
diff --git a/openstack_dashboard/dashboards/project/stacks/forms.py b/openstack_dashboard/dashboards/project/stacks/forms.py
deleted file mode 100644
index e8b9702..0000000
--- a/openstack_dashboard/dashboards/project/stacks/forms.py
+++ /dev/null
@@ -1,488 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import json
14import logging
15
16import django
17from django.conf import settings
18from django.utils import html
19from django.utils.translation import ugettext_lazy as _
20from django.views.decorators.debug import sensitive_variables
21
22from oslo_utils import strutils
23import six
24
25from horizon import exceptions
26from horizon import forms
27from horizon import messages
28
29from openstack_dashboard import api
30from openstack_dashboard.dashboards.project.images \
31 import utils as image_utils
32from openstack_dashboard.dashboards.project.instances \
33 import utils as instance_utils
34
35
36LOG = logging.getLogger(__name__)
37
38
39def create_upload_form_attributes(prefix, input_type, name):
40 """Creates attribute dicts for the switchable upload form
41
42 :type prefix: str
43 :param prefix: prefix (environment, template) of field
44 :type input_type: str
45 :param input_type: field type (file, raw, url)
46 :type name: str
47 :param name: translated text label to display to user
48 :rtype: dict
49 :return: an attribute set to pass to form build
50 """
51 attributes = {'class': 'switched', 'data-switch-on': prefix + 'source'}
52 attributes['data-' + prefix + 'source-' + input_type] = name
53 return attributes
54
55
56class TemplateForm(forms.SelfHandlingForm):
57
58 class Meta(object):
59 name = _('Select Template')
60 help_text = _('Select a template to launch a stack.')
61
62 # TODO(jomara) - update URL choice for template & environment files
63 # w/ client side download when applicable
64 base_choices = [('file', _('File')),
65 ('raw', _('Direct Input'))]
66 url_choice = [('url', _('URL'))]
67 attributes = {'class': 'switchable', 'data-slug': 'templatesource'}
68 template_source = forms.ChoiceField(label=_('Template Source'),
69 choices=base_choices + url_choice,
70 widget=forms.ThemableSelectWidget(
71 attrs=attributes))
72
73 attributes = create_upload_form_attributes(
74 'template',
75 'file',
76 _('Template File'))
77 template_upload = forms.FileField(
78 label=_('Template File'),
79 help_text=_('A local template to upload.'),
80 widget=forms.FileInput(attrs=attributes),
81 required=False)
82
83 attributes = create_upload_form_attributes(
84 'template',
85 'url',
86 _('Template URL'))
87 template_url = forms.URLField(
88 label=_('Template URL'),
89 help_text=_('An external (HTTP) URL to load the template from.'),
90 widget=forms.TextInput(attrs=attributes),
91 required=False)
92
93 attributes = create_upload_form_attributes(
94 'template',
95 'raw',
96 _('Template Data'))
97 template_data = forms.CharField(
98 label=_('Template Data'),
99 help_text=_('The raw contents of the template.'),
100 widget=forms.widgets.Textarea(attrs=attributes),
101 required=False)
102
103 attributes = {'data-slug': 'envsource', 'class': 'switchable'}
104 environment_source = forms.ChoiceField(
105 label=_('Environment Source'),
106 choices=base_choices,
107 widget=forms.ThemableSelectWidget(attrs=attributes),
108 required=False)
109
110 attributes = create_upload_form_attributes(
111 'env',
112 'file',
113 _('Environment File'))
114 environment_upload = forms.FileField(
115 label=_('Environment File'),
116 help_text=_('A local environment to upload.'),
117 widget=forms.FileInput(attrs=attributes),
118 required=False)
119
120 attributes = create_upload_form_attributes(
121 'env',
122 'raw',
123 _('Environment Data'))
124 environment_data = forms.CharField(
125 label=_('Environment Data'),
126 help_text=_('The raw contents of the environment file.'),
127 widget=forms.widgets.Textarea(attrs=attributes),
128 required=False)
129
130 if django.VERSION >= (1, 9):
131 # Note(Itxaka): On django>=1.9 Charfield has an strip option that
132 # we need to set to False as to not hit
133 # https://bugs.launchpad.net/python-heatclient/+bug/1546166
134 environment_data.strip = False
135 template_data.strip = False
136
137 def __init__(self, *args, **kwargs):
138 self.next_view = kwargs.pop('next_view')
139 super(TemplateForm, self).__init__(*args, **kwargs)
140
141 def clean(self):
142 cleaned = super(TemplateForm, self).clean()
143
144 files = self.request.FILES
145 self.clean_uploaded_files('template', _('template'), cleaned, files)
146 self.clean_uploaded_files('environment', _('environment'), cleaned,
147 files)
148
149 # Validate the template and get back the params.
150 kwargs = {}
151 if cleaned['environment_data']:
152 kwargs['environment'] = cleaned['environment_data']
153 try:
154 files, tpl =\
155 api.heat.get_template_files(cleaned.get('template_data'),
156 cleaned.get('template_url'))
157 kwargs['files'] = files
158 kwargs['template'] = tpl
159 validated = api.heat.template_validate(self.request, **kwargs)
160 cleaned['template_validate'] = validated
161 cleaned['template_validate']['files'] = files
162 cleaned['template_validate']['template'] = tpl
163 except Exception as e:
164 raise forms.ValidationError(six.text_type(e))
165
166 return cleaned
167
168 def clean_uploaded_files(self, prefix, field_label, cleaned, files):
169 """Cleans Template & Environment data from form upload.
170
171 Does some of the crunchy bits for processing uploads vs raw
172 data depending on what the user specified. Identical process
173 for environment data & template data.
174
175 :type prefix: str
176 :param prefix: prefix (environment, template) of field
177 :type field_label: str
178 :param field_label: translated prefix str for messages
179 :type input_type: dict
180 :param prefix: existing cleaned fields from form
181 :rtype: dict
182 :return: cleaned dict including environment & template data
183 """
184
185 upload_str = prefix + "_upload"
186 data_str = prefix + "_data"
187 url = cleaned.get(prefix + '_url')
188 data = cleaned.get(prefix + '_data')
189
190 has_upload = upload_str in files
191 # Uploaded file handler
192 if has_upload and not url:
193 log_template_name = files[upload_str].name
194 LOG.info('got upload %s', log_template_name)
195
196 tpl = files[upload_str].read()
197 if tpl.startswith('{'):
198 try:
199 json.loads(tpl)
200 except Exception as e:
201 msg = _('There was a problem parsing the'
202 ' %(prefix)s: %(error)s')
203 msg = msg % {'prefix': prefix, 'error': six.text_type(e)}
204 raise forms.ValidationError(msg)
205 cleaned[data_str] = tpl
206
207 # URL handler
208 elif url and (has_upload or data):
209 msg = _('Please specify a %s using only one source method.')
210 msg = msg % field_label
211 raise forms.ValidationError(msg)
212
213 elif prefix == 'template':
214 # Check for raw template input - blank environment allowed
215 if not url and not data:
216 msg = _('You must specify a template via one of the '
217 'available sources.')
218 raise forms.ValidationError(msg)
219
220 def create_kwargs(self, data):
221 kwargs = {'parameters': data['template_validate'],
222 'environment_data': data['environment_data']}
223 if data.get('stack_id'):
224 kwargs['stack_id'] = data['stack_id']
225 return kwargs
226
227 def handle(self, request, data):
228 kwargs = self.create_kwargs(data)
229 # NOTE (gabriel): This is a bit of a hack, essentially rewriting this
230 # request so that we can chain it as an input to the next view...
231 # but hey, it totally works.
232 request.method = 'GET'
233
234 return self.next_view.as_view()(request, **kwargs)
235
236
237class ChangeTemplateForm(TemplateForm):
238 class Meta(object):
239 name = _('Edit Template')
240 help_text = _('Select a new template to re-launch a stack.')
241 stack_id = forms.CharField(label=_('Stack ID'),
242 widget=forms.widgets.HiddenInput)
243 stack_name = forms.CharField(label=_('Stack Name'),
244 widget=forms.TextInput(attrs={'readonly':
245 'readonly'}))
246
247
248class PreviewTemplateForm(TemplateForm):
249 class Meta(object):
250 name = _('Preview Template')
251 help_text = _('Select a new template to preview a stack.')
252
253
254class CreateStackForm(forms.SelfHandlingForm):
255
256 param_prefix = '__param_'
257
258 class Meta(object):
259 name = _('Create Stack')
260
261 environment_data = forms.CharField(
262 widget=forms.widgets.HiddenInput,
263 required=False)
264 if django.VERSION >= (1, 9):
265 # Note(Itxaka): On django>=1.9 Charfield has an strip option that
266 # we need to set to False as to not hit
267 # https://bugs.launchpad.net/python-heatclient/+bug/1546166
268 environment_data.strip = False
269
270 parameters = forms.CharField(
271 widget=forms.widgets.HiddenInput)
272 stack_name = forms.RegexField(
273 max_length=255,
274 label=_('Stack Name'),
275 help_text=_('Name of the stack to create.'),
276 regex=r"^[a-zA-Z][a-zA-Z0-9_.-]*$",
277 error_messages={'invalid':
278 _('Name must start with a letter and may '
279 'only contain letters, numbers, underscores, '
280 'periods and hyphens.')})
281 timeout_mins = forms.IntegerField(
282 initial=60,
283 label=_('Creation Timeout (minutes)'),
284 help_text=_('Stack creation timeout in minutes.'))
285 enable_rollback = forms.BooleanField(
286 label=_('Rollback On Failure'),
287 help_text=_('Enable rollback on create/update failure.'),
288 required=False)
289
290 def __init__(self, *args, **kwargs):
291 parameters = kwargs.pop('parameters')
292 # special case: load template data from API, not passed in params
293 if kwargs.get('validate_me'):
294 parameters = kwargs.pop('validate_me')
295 super(CreateStackForm, self).__init__(*args, **kwargs)
296
297 if self._stack_password_enabled():
298 self.fields['password'] = forms.CharField(
299 label=_('Password for user "%s"') % self.request.user.username,
300 help_text=_('This is required for operations to be performed '
301 'throughout the lifecycle of the stack'),
302 widget=forms.PasswordInput())
303
304 self._build_parameter_fields(parameters)
305
306 def _stack_password_enabled(self):
307 stack_settings = getattr(settings, 'OPENSTACK_HEAT_STACK', {})
308 return stack_settings.get('enable_user_pass', True)
309
310 def _build_parameter_fields(self, template_validate):
311 self.help_text = template_validate['Description']
312
313 params = template_validate.get('Parameters', {})
314 if template_validate.get('ParameterGroups'):
315 params_in_order = []
316 for group in template_validate['ParameterGroups']:
317 for param in group.get('parameters', []):
318 if param in params:
319 params_in_order.append((param, params[param]))
320 else:
321 # no parameter groups, simply sorted to make the order fixed
322 params_in_order = sorted(params.items())
323 for param_key, param in params_in_order:
324 field = None
325 field_key = self.param_prefix + param_key
326 initial = param.get('Value',
327 param.get('DefaultValue',
328 param.get('Default')))
329 field_args = {
330 'initial': initial,
331 'label': param.get('Label', param_key),
332 'help_text': html.escape(param.get('Description', '')),
333 'required': initial is None,
334 }
335
336 param_type = param.get('Type', None)
337 hidden = strutils.bool_from_string(param.get('NoEcho', 'false'))
338 if 'CustomConstraint' in param:
339 choices = self._populate_custom_choices(
340 param['CustomConstraint'])
341 field_args['choices'] = choices
342 field = forms.ChoiceField(**field_args)
343
344 elif 'AllowedValues' in param:
345 choices = map(lambda x: (x, x), param['AllowedValues'])
346 field_args['choices'] = choices
347 field = forms.ChoiceField(**field_args)
348
349 elif param_type == 'Json' and 'Default' in param:
350 field_args['initial'] = json.dumps(param['Default'])
351 field = forms.CharField(**field_args)
352
353 elif param_type in ('CommaDelimitedList', 'String', 'Json'):
354 if 'MinLength' in param:
355 field_args['min_length'] = int(param['MinLength'])
356 field_args['required'] = field_args['min_length'] > 0
357 if 'MaxLength' in param:
358 field_args['max_length'] = int(param['MaxLength'])
359 if hidden:
360 field_args['widget'] = forms.PasswordInput(
361 render_value=True)
362 field = forms.CharField(**field_args)
363
364 elif param_type == 'Number':
365 if 'MinValue' in param:
366 field_args['min_value'] = int(param['MinValue'])
367 if 'MaxValue' in param:
368 field_args['max_value'] = int(param['MaxValue'])
369 field = forms.IntegerField(**field_args)
370
371 elif param_type == 'Boolean':
372 field_args['required'] = False
373 field = forms.BooleanField(**field_args)
374
375 if field:
376 self.fields[field_key] = field
377
378 @sensitive_variables('password')
379 def handle(self, request, data):
380 prefix_length = len(self.param_prefix)
381 params_list = [(k[prefix_length:], v) for (k, v) in data.items()
382 if k.startswith(self.param_prefix)]
383 fields = {
384 'stack_name': data.get('stack_name'),
385 'timeout_mins': data.get('timeout_mins'),
386 'disable_rollback': not(data.get('enable_rollback')),
387 'parameters': dict(params_list),
388 'files': json.loads(data.get('parameters')).get('files'),
389 'template': json.loads(data.get('parameters')).get('template')
390 }
391 if data.get('password'):
392 fields['password'] = data.get('password')
393
394 if data.get('environment_data'):
395 fields['environment'] = data.get('environment_data')
396
397 try:
398 api.heat.stack_create(self.request, **fields)
399 messages.info(request, _("Stack creation started."))
400 return True
401 except Exception:
402 exceptions.handle(request)
403
404 def _populate_custom_choices(self, custom_type):
405 if custom_type == 'neutron.network':
406 return instance_utils.network_field_data(self.request, True)
407 if custom_type == 'nova.keypair':
408 return instance_utils.keypair_field_data(self.request, True)
409 if custom_type == 'glance.image':
410 return image_utils.image_field_data(self.request, True)
411 if custom_type == 'nova.flavor':
412 return instance_utils.flavor_field_data(self.request, True)
413 return []
414
415
416class EditStackForm(CreateStackForm):
417
418 class Meta(object):
419 name = _('Update Stack Parameters')
420
421 stack_id = forms.CharField(
422 label=_('Stack ID'),
423 widget=forms.widgets.HiddenInput)
424 stack_name = forms.CharField(
425 label=_('Stack Name'),
426 widget=forms.TextInput(attrs={'readonly': 'readonly'}))
427
428 @sensitive_variables('password')
429 def handle(self, request, data):
430 prefix_length = len(self.param_prefix)
431 params_list = [(k[prefix_length:], v) for (k, v) in data.items()
432 if k.startswith(self.param_prefix)]
433
434 stack_id = data.get('stack_id')
435 fields = {
436 'stack_name': data.get('stack_name'),
437 'timeout_mins': data.get('timeout_mins'),
438 'disable_rollback': not(data.get('enable_rollback')),
439 'parameters': dict(params_list),
440 'files': json.loads(data.get('parameters')).get('files'),
441 'template': json.loads(data.get('parameters')).get('template')
442 }
443 if data.get('password'):
444 fields['password'] = data.get('password')
445
446 if data.get('environment_data'):
447 fields['environment'] = data.get('environment_data')
448
449 try:
450 api.heat.stack_update(self.request, stack_id=stack_id, **fields)
451 messages.info(request, _("Stack update started."))
452 return True
453 except Exception:
454 exceptions.handle(request)
455
456
457class PreviewStackForm(CreateStackForm):
458
459 class Meta(object):
460 name = _('Preview Stack Parameters')
461
462 def __init__(self, *args, **kwargs):
463 self.next_view = kwargs.pop('next_view')
464 super(CreateStackForm, self).__init__(*args, **kwargs)
465
466 def handle(self, request, data):
467 prefix_length = len(self.param_prefix)
468 params_list = [(k[prefix_length:], v) for (k, v) in data.items()
469 if k.startswith(self.param_prefix)]
470 fields = {
471 'stack_name': data.get('stack_name'),
472 'timeout_mins': data.get('timeout_mins'),
473 'disable_rollback': not(data.get('enable_rollback')),
474 'parameters': dict(params_list),
475 'files': json.loads(data.get('parameters')).get('files'),
476 'template': json.loads(data.get('parameters')).get('template')
477 }
478
479 if data.get('environment_data'):
480 fields['environment'] = data.get('environment_data')
481
482 try:
483 stack_preview = api.heat.stack_preview(self.request, **fields)
484 request.method = 'GET'
485 return self.next_view.as_view()(request,
486 stack_preview=stack_preview)
487 except Exception:
488 exceptions.handle(request)
diff --git a/openstack_dashboard/dashboards/project/stacks/mappings.py b/openstack_dashboard/dashboards/project/stacks/mappings.py
deleted file mode 100644
index eeab18c..0000000
--- a/openstack_dashboard/dashboards/project/stacks/mappings.py
+++ /dev/null
@@ -1,350 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import json
14import logging
15
16from django.conf import settings
17from django.core.urlresolvers import reverse
18from django.template.defaultfilters import register
19from django.utils import html
20from django.utils import safestring
21import six
22import six.moves.urllib.parse as urlparse
23
24from openstack_dashboard.api import swift
25
26LOG = logging.getLogger(__name__)
27
28
29resource_urls = {
30 "AWS::AutoScaling::AutoScalingGroup": {
31 'link': 'horizon:project:stacks:detail'},
32 "AWS::CloudFormation::Stack": {
33 'link': 'horizon:project:stacks:detail'},
34 "AWS::EC2::Instance": {
35 'link': 'horizon:project:instances:detail'},
36 "AWS::EC2::InternetGateway": {
37 'link': 'horizon:project:networks:ports:detail'},
38 "AWS::EC2::NetworkInterface": {
39 'link': 'horizon:project:networks:ports:detail'},
40 "AWS::EC2::RouteTable": {
41 'link': 'horizon:project:routers:detail'},
42 "AWS::EC2::SecurityGroup": {
43 'link': 'horizon:project:security_groups:index'},
44 "AWS::EC2::Subnet": {
45 'link': 'horizon:project:networks:subnets:detail'},
46 "AWS::EC2::Volume": {
47 'link': 'horizon:project:volumes:detail'},
48 "AWS::EC2::VPC": {
49 'link': 'horizon:project:networks:detail'},
50 "AWS::S3::Bucket": {
51 'link': 'horizon:project:containers:index'},
52 "OS::Cinder::Volume": {
53 'link': 'horizon:project:volumes:detail'},
54 "OS::Heat::AccessPolicy": {
55 'link': 'horizon:project:stacks:detail'},
56 "OS::Heat::AutoScalingGroup": {
57 'link': 'horizon:project:stacks:detail'},
58 "OS::Heat::CloudConfig": {
59 'link': 'horizon:project:stacks:detail'},
60 "OS::Neutron::Firewall": {
61 'link': 'horizon:project:firewalls:firewalldetails'},
62 "OS::Neutron::FirewallPolicy": {
63 'link': 'horizon:project:firewalls:policydetails'},
64 "OS::Neutron::FirewallRule": {
65 'link': 'horizon:project:firewalls:ruledetails'},
66 "OS::Heat::HARestarter": {
67 'link': 'horizon:project:stacks:detail'},
68 "OS::Heat::InstanceGroup": {
69 'link': 'horizon:project:stacks:detail'},
70 "OS::Heat::MultipartMime": {
71 'link': 'horizon:project:stacks:detail'},
72 "OS::Heat::ResourceGroup": {
73 'link': 'horizon:project:stacks:detail'},
74 "OS::Heat::SoftwareConfig": {
75 'link': 'horizon:project:stacks:detail'},
76 "OS::Heat::StructuredConfig": {
77 'link': 'horizon:project:stacks:detail'},
78 "OS::Heat::StructuredDeployment": {
79 'link': 'horizon:project:stacks:detail'},
80 "OS::Heat::Stack": {
81 'link': 'horizon:project:stacks:detail'},
82 "OS::Heat::WaitCondition": {
83 'link': 'horizon:project:stacks:detail'},
84 "OS::Heat::WaitConditionHandle": {
85 'link': 'horizon:project:stacks:detail'},
86 "OS::Neutron::IKEPolicy": {
87 'link': 'horizon:project:vpn:ikepolicydetails'},
88 "OS::Neutron::IPsecPolicy": {
89 'link': 'horizon:project:vpn:ipsecpolicydetails'},
90 "OS::Neutron::IPsecSiteConnection": {
91 'link': 'horizon:project:vpn:ipsecsiteconnectiondetails'},
92 "OS::Neutron::Net": {
93 'link': 'horizon:project:networks:detail'},
94 "OS::Neutron::Port": {
95 'link': 'horizon:project:networks:ports:detail'},
96 "OS::Neutron::Router": {
97 'link': 'horizon:project:routers:detail'},
98 "OS::Neutron::Subnet": {
99 'link': 'horizon:project:networks:subnets:detail'},
100 "OS::Neutron::VPNService": {
101 'link': 'horizon:project:vpn:vpnservicedetails'},
102 "OS::Nova::KeyPair": {
103 'link': 'horizon:project:key_pairs:index'},
104 "OS::Nova::Server": {
105 'link': 'horizon:project:instances:detail'},
106 "OS::Swift::Container": {
107 'link': 'horizon:project:containers:index',
108 'format_pattern': '%s' + swift.FOLDER_DELIMITER},
109}
110
111
112def resource_to_url(resource):
113 if (not resource or
114 not resource.physical_resource_id or
115 not hasattr(resource, 'resource_type')):
116 return None
117
118 mapping = resource_urls.get(resource.resource_type, {})
119 try:
120 if 'link' not in mapping:
121 return None
122 format_pattern = mapping.get('format_pattern') or '%s'
123 rid = format_pattern % resource.physical_resource_id
124 url = reverse(mapping['link'], args=(rid,))
125 except Exception as e:
126 LOG.exception(e)
127 return None
128 return url
129
130
131@register.filter
132def stack_output(output):
133 if not output:
134 return u''
135 if isinstance(output, six.string_types):
136 parts = urlparse.urlsplit(output)
137 if parts.netloc and parts.scheme in ('http', 'https'):
138 url = html.escape(output)
139 safe_link = u'<a href="%s" target="_blank">%s</a>' % (url, url)
140 return safestring.mark_safe(safe_link)
141 if isinstance(output, dict) or isinstance(output, list):
142 output = json.dumps(output, indent=2)
143 return safestring.mark_safe(u'<pre>%s</pre>' % html.escape(output))
144
145static_url = getattr(settings, "STATIC_URL", "/static/")
146resource_images = {
147 'LB_FAILED': static_url + 'dashboard/img/lb-red.svg',
148 'LB_DELETE': static_url + 'dashboard/img/lb-red.svg',
149 'LB_IN_PROGRESS': static_url + 'dashboard/img/lb-gray.gif',
150 'LB_INIT': static_url + 'dashboard/img/lb-gray.svg',
151 'LB_COMPLETE': static_url + 'dashboard/img/lb-green.svg',
152 'DB_FAILED': static_url + 'dashboard/img/db-red.svg',
153 'DB_DELETE': static_url + 'dashboard/img/db-red.svg',
154 'DB_IN_PROGRESS': static_url + 'dashboard/img/db-gray.gif',
155 'DB_INIT': static_url + 'dashboard/img/db-gray.svg',
156 'DB_COMPLETE': static_url + 'dashboard/img/db-green.svg',
157 'STACK_FAILED': static_url + 'dashboard/img/stack-red.svg',
158 'STACK_DELETE': static_url + 'dashboard/img/stack-red.svg',
159 'STACK_IN_PROGRESS': static_url + 'dashboard/img/stack-gray.gif',
160 'STACK_INIT': static_url + 'dashboard/img/stack-gray.svg',
161 'STACK_COMPLETE': static_url + 'dashboard/img/stack-green.svg',
162 'SERVER_FAILED': static_url + 'dashboard/img/server-red.svg',
163 'SERVER_DELETE': static_url + 'dashboard/img/server-red.svg',
164 'SERVER_IN_PROGRESS': static_url + 'dashboard/img/server-gray.gif',
165 'SERVER_INIT': static_url + 'dashboard/img/server-gray.svg',
166 'SERVER_COMPLETE': static_url + 'dashboard/img/server-green.svg',
167 'ALARM_FAILED': static_url + 'dashboard/img/alarm-red.svg',
168 'ALARM_DELETE': static_url + 'dashboard/img/alarm-red.svg',
169 'ALARM_IN_PROGRESS': static_url + 'dashboard/img/alarm-gray.gif',
170 'ALARM_INIT': static_url + 'dashboard/img/alarm-gray.svg',
171 'ALARM_COMPLETE': static_url + 'dashboard/img/alarm-green.svg',
172 'VOLUME_FAILED': static_url + 'dashboard/img/volume-red.svg',
173 'VOLUME_DELETE': static_url + 'dashboard/img/volume-red.svg',
174 'VOLUME_IN_PROGRESS': static_url + 'dashboard/img/volume-gray.gif',
175 'VOLUME_INIT': static_url + 'dashboard/img/volume-gray.svg',
176 'VOLUME_COMPLETE': static_url + 'dashboard/img/volume-green.svg',
177 'IMAGE_FAILED': static_url + 'dashboard/img/image-red.svg',
178 'IMAGE_DELETE': static_url + 'dashboard/img/image-red.svg',
179 'IMAGE_IN_PROGRESS': static_url + 'dashboard/img/image-gray.gif',
180 'IMAGE_INIT': static_url + 'dashboard/img/image-gray.svg',
181 'IMAGE_COMPLETE': static_url + 'dashboard/img/image-green.svg',
182 'WAIT_FAILED': static_url + 'dashboard/img/wait-red.svg',
183 'WAIT_DELETE': static_url + 'dashboard/img/wait-red.svg',
184 'WAIT_IN_PROGRESS': static_url + 'dashboard/img/wait-gray.gif',
185 'WAIT_INIT': static_url + 'dashboard/img/wait-gray.svg',
186 'WAIT_COMPLETE': static_url + 'dashboard/img/wait-green.svg',
187 'FIREWALL_FAILED': static_url + 'dashboard/img/firewall-red.svg',
188 'FIREWALL_DELETE': static_url + 'dashboard/img/firewall-red.svg',
189 'FIREWALL_IN_PROGRESS': static_url + 'dashboard/img/firewall-gray.gif',
190 'FIREWALL_INIT': static_url + 'dashboard/img/firewall-gray.svg',
191 'FIREWALL_COMPLETE': static_url + 'dashboard/img/firewall-green.svg',
192 'FLOATINGIP_FAILED': static_url + 'dashboard/img/floatingip-red.svg',
193 'FLOATINGIP_DELETE': static_url + 'dashboard/img/floatingip-red.svg',
194 'FLOATINGIP_IN_PROGRESS': static_url + 'dashboard/img/floatingip-gray.gif',
195 'FLOATINGIP_INIT': static_url + 'dashboard/img/floatingip-gray.svg',
196 'FLOATINGIP_COMPLETE': static_url + 'dashboard/img/floatingip-green.svg',
197 'ROUTER_FAILED': static_url + 'dashboard/img/router-red.svg',
198 'ROUTER_DELETE': static_url + 'dashboard/img/router-red.svg',
199 'ROUTER_IN_PROGRESS': static_url + 'dashboard/img/router-gray.gif',
200 'ROUTER_INIT': static_url + 'dashboard/img/router-gray.svg',
201 'ROUTER_COMPLETE': static_url + 'dashboard/img/router-green.svg',
202 'POLICY_FAILED': static_url + 'dashboard/img/policy-red.svg',
203 'POLICY_DELETE': static_url + 'dashboard/img/policy-red.svg',
204 'POLICY_IN_PROGRESS': static_url + 'dashboard/img/policy-gray.gif',
205 'POLICY_INIT': static_url + 'dashboard/img/policy-gray.svg',
206 'POLICY_COMPLETE': static_url + 'dashboard/img/policy-green.svg',
207 'CONFIG_FAILED': static_url + 'dashboard/img/config-red.svg',
208 'CONFIG_DELETE': static_url + 'dashboard/img/config-red.svg',
209 'CONFIG_IN_PROGRESS': static_url + 'dashboard/img/config-gray.gif',
210 'CONFIG_INIT': static_url + 'dashboard/img/config-gray.svg',
211 'CONFIG_COMPLETE': static_url + 'dashboard/img/config-green.svg',
212 'NETWORK_FAILED': static_url + 'dashboard/img/network-red.svg',
213 'NETWORK_DELETE': static_url + 'dashboard/img/network-red.svg',
214 'NETWORK_IN_PROGRESS': static_url + 'dashboard/img/network-gray.gif',
215 'NETWORK_INIT': static_url + 'dashboard/img/network-gray.svg',
216 'NETWORK_COMPLETE': static_url + 'dashboard/img/network-green.svg',
217 'PORT_FAILED': static_url + 'dashboard/img/port-red.svg',
218 'PORT_DELETE': static_url + 'dashboard/img/port-red.svg',
219 'PORT_IN_PROGRESS': static_url + 'dashboard/img/port-gray.gif',
220 'PORT_INIT': static_url + 'dashboard/img/port-gray.svg',
221 'PORT_COMPLETE': static_url + 'dashboard/img/port-green.svg',
222 'SECURITYGROUP_FAILED': static_url + 'dashboard/img/securitygroup-red.svg',
223 'SECURITYGROUP_DELETE': static_url + 'dashboard/img/securitygroup-red.svg',
224 'SECURITYGROUP_IN_PROGRESS':
225 static_url + 'dashboard/img/securitygroup-gray.gif',
226 'SECURITYGROUP_INIT': static_url + 'dashboard/img/securitygroup-gray.svg',
227 'SECURITYGROUP_COMPLETE':
228 static_url + 'dashboard/img/securitygroup-green.svg',
229 'VPN_FAILED': static_url + 'dashboard/img/vpn-red.svg',
230 'VPN_DELETE': static_url + 'dashboard/img/vpn-red.svg',
231 'VPN_IN_PROGRESS': static_url + 'dashboard/img/vpn-gray.gif',
232 'VPN_INIT': static_url + 'dashboard/img/vpn-gray.svg',
233 'VPN_COMPLETE': static_url + 'dashboard/img/vpn-green.svg',
234 'FLAVOR_FAILED': static_url + 'dashboard/img/flavor-red.svg',
235 'FLAVOR_DELETE': static_url + 'dashboard/img/flavor-red.svg',
236 'FLAVOR_IN_PROGRESS': static_url + 'dashboard/img/flavor-gray.gif',
237 'FLAVOR_INIT': static_url + 'dashboard/img/flavor-gray.svg',
238 'FLAVOR_COMPLETE': static_url + 'dashboard/img/flavor-green.svg',
239 'KEYPAIR_FAILED': static_url + 'dashboard/img/keypair-red.svg',
240 'KEYPAIR_DELETE': static_url + 'dashboard/img/keypair-red.svg',
241 'KEYPAIR_IN_PROGRESS': static_url + 'dashboard/img/keypair-gray.gif',
242 'KEYPAIR_INIT': static_url + 'dashboard/img/keypair-gray.svg',
243 'KEYPAIR_COMPLETE': static_url + 'dashboard/img/keypair-green.svg',
244 'UNKNOWN_FAILED': static_url + 'dashboard/img/unknown-red.svg',
245 'UNKNOWN_DELETE': static_url + 'dashboard/img/unknown-red.svg',
246 'UNKNOWN_IN_PROGRESS': static_url + 'dashboard/img/unknown-gray.gif',
247 'UNKNOWN_INIT': static_url + 'dashboard/img/unknown-gray.svg',
248 'UNKNOWN_COMPLETE': static_url + 'dashboard/img/unknown-green.svg',
249}
250
251
252resource_types = {
253 # LB
254 'LoadBalance': 'LB',
255 'HealthMonitor': 'LB',
256 'PoolMember': 'LB',
257 'Pool': 'LB',
258 # DB
259 'DBInstance': 'DB',
260 'Database': 'DB',
261 # SERVER
262 'Instance': 'SERVER',
263 'Server': 'SERVER',
264 # ALARM
265 'Alarm': 'ALARM',
266 'CombinationAlarm': 'ALARM',
267 'CWLiteAlarm': 'ALARM',
268 # VOLUME
269 'Volume': 'VOLUME',
270 'VolumeAttachment': 'VOLUME',
271 # STACK
272 'stack': 'STACK',
273 'AutoScalingGroup': 'STACK',
274 'InstanceGroup': 'STACK',
275 'ServerGroup': 'STACK',
276 'ResourceGroup': 'STACK',
277 # IMAGE
278 'Image': 'IMAGE',
279 # WAIT
280 'WaitCondition': 'WAIT',
281 'WaitConditionHandle': 'WAIT',
282 'UpdateWaitConditionHandle': 'WAIT',
283 # FIREWALL
284 'Firewall': 'FIREWALL',
285 'FirewallPolicy': 'FIREWALL',
286 'FirewallRule': 'FIREWALL',
287 # FLOATINGIP
288 'FloatingIP': 'FLOATINGIP',
289 'FloatingIPAssociation': 'FLOATINGIP',
290 # ROUTER
291 'Router': 'ROUTER',
292 'RouterGateway': 'ROUTER',
293 'RouterInterface': 'ROUTER',
294 # POLICY
295 'ScalingPolicy': 'POLICY',
296 # CONFIG
297 'CloudConfig': 'CONFIG',
298 'MultipartMime': 'CONFIG',
299 'SoftwareConfig': 'CONFIG',
300 'SoftwareDeployment': 'CONFIG',
301 'StructuredConfig': 'CONFIG',
302 'StructuredDeployment': 'CONFIG',
303 # NETWORK
304 'Net': 'NETWORK',
305 'Subnet': 'NETWORK',
306 'NetworkGateway': 'NETWORK',
307 'ProviderNet': 'NETWORK',
308 # PORT
309 'Port': 'PORT',
310 # SECURITYGROUP
311 'SecurityGroup': 'SECURITYGROUP',
312 # VPN
313 'VPNService': 'VPN',
314 # FLAVOR
315 'Flavor': 'FLAVOR',
316 # KEYPAIR
317 'KeyPair': 'KEYPAIR',
318}
319
320
321def get_resource_type(type):
322 for key, value in resource_types.items():
323 if key in type:
324 return value
325
326 return 'UNKNOWN'
327
328
329def get_resource_status(status):
330 if ('IN_PROGRESS' in status):
331 return 'IN_PROGRESS'
332 elif ('FAILED' in status):
333 return 'FAILED'
334 elif ('DELETE' in status):
335 return 'DELETE'
336 elif ('INIT' in status):
337 return 'INIT'
338 else:
339 return 'COMPLETE'
340
341
342def get_resource_image(status, type):
343 """Sets the image url and in_progress action sw based on status."""
344 resource_type = get_resource_type(type)
345 resource_status = get_resource_status(status)
346 resource_state = resource_type + "_" + resource_status
347
348 for key in resource_images:
349 if key == resource_state:
350 return resource_images.get(key)
diff --git a/openstack_dashboard/dashboards/project/stacks/panel.py b/openstack_dashboard/dashboards/project/stacks/panel.py
deleted file mode 100644
index f9e8800..0000000
--- a/openstack_dashboard/dashboards/project/stacks/panel.py
+++ /dev/null
@@ -1,21 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13from django.utils.translation import ugettext_lazy as _
14
15import horizon
16
17
18class Stacks(horizon.Panel):
19 name = _("Stacks")
20 slug = "stacks"
21 permissions = ('openstack.services.orchestration',)
diff --git a/openstack_dashboard/dashboards/project/stacks/resource_types/__init__.py b/openstack_dashboard/dashboards/project/stacks/resource_types/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/openstack_dashboard/dashboards/project/stacks/resource_types/__init__.py
+++ /dev/null
diff --git a/openstack_dashboard/dashboards/project/stacks/resource_types/panel.py b/openstack_dashboard/dashboards/project/stacks/resource_types/panel.py
deleted file mode 100644
index b1f3744..0000000
--- a/openstack_dashboard/dashboards/project/stacks/resource_types/panel.py
+++ /dev/null
@@ -1,23 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10# implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14from django.utils.translation import ugettext_lazy as _
15
16import horizon
17
18
19class ResourceTypes(horizon.Panel):
20 name = _("Resource Types")
21 slug = "stacks.resource_types"
22 permissions = ('openstack.services.orchestration',)
23 policy_rules = (("orchestration", "stacks:list_resource_types"),)
diff --git a/openstack_dashboard/dashboards/project/stacks/resource_types/tables.py b/openstack_dashboard/dashboards/project/stacks/resource_types/tables.py
deleted file mode 100644
index 66e7702..0000000
--- a/openstack_dashboard/dashboards/project/stacks/resource_types/tables.py
+++ /dev/null
@@ -1,36 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10# implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14from django.utils.translation import ugettext_lazy as _
15
16from horizon import tables
17
18
19class ResourceTypesFilterAction(tables.FilterAction):
20 filter_type = 'server'
21 filter_choices = (('name', _('Type ='), True, _("Case sensitive")),)
22
23
24class ResourceTypesTable(tables.DataTable):
25 name = tables.Column("resource_type",
26 verbose_name=_("Type"),
27 link="horizon:project:stacks.resource_types:details",)
28
29 def get_object_id(self, resource):
30 return resource.resource_type
31
32 class Meta(object):
33 name = "resource_types"
34 verbose_name = _("Resource Types")
35 table_actions = (ResourceTypesFilterAction,)
36 multi_select = False
diff --git a/openstack_dashboard/dashboards/project/stacks/resource_types/tabs.py b/openstack_dashboard/dashboards/project/stacks/resource_types/tabs.py
deleted file mode 100644
index b0d5467..0000000
--- a/openstack_dashboard/dashboards/project/stacks/resource_types/tabs.py
+++ /dev/null
@@ -1,32 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10# implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14from django.utils.translation import ugettext_lazy as _
15
16from horizon import tabs
17
18
19class ResourceTypeOverviewTab(tabs.Tab):
20 name = _("Overview")
21 slug = "resource_type_overview"
22 template_name = "project/stacks.resource_types/_details.html"
23
24 def get_context_data(self, request):
25 return {"r_type": self.tab_group.kwargs['rt'],
26 "r_type_attributes": self.tab_group.kwargs['rt_attributes'],
27 "r_type_properties": self.tab_group.kwargs['rt_properties']}
28
29
30class ResourceTypeDetailsTabs(tabs.TabGroup):
31 slug = "resource_type_details"
32 tabs = (ResourceTypeOverviewTab,)
diff --git a/openstack_dashboard/dashboards/project/stacks/resource_types/templates/stacks.resource_types/_details.html b/openstack_dashboard/dashboards/project/stacks/resource_types/templates/stacks.resource_types/_details.html
deleted file mode 100644
index 704787e..0000000
--- a/openstack_dashboard/dashboards/project/stacks/resource_types/templates/stacks.resource_types/_details.html
+++ /dev/null
@@ -1,15 +0,0 @@
1{% load i18n %}
2
3<div class="detail">
4 <dl>
5 <dd>{{ r_type }}</dd>
6 </dl>
7
8 <h4>{% trans "Attributes" %}</h4>
9 <pre>{{ r_type_attributes }}
10 </pre>
11
12 <h4>{% trans "Properties" %}</h4>
13 <pre>{{ r_type_properties }}
14 </pre>
15</div>
diff --git a/openstack_dashboard/dashboards/project/stacks/resource_types/tests.py b/openstack_dashboard/dashboards/project/stacks/resource_types/tests.py
deleted file mode 100644
index 4c73644..0000000
--- a/openstack_dashboard/dashboards/project/stacks/resource_types/tests.py
+++ /dev/null
@@ -1,52 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10# implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14from django.core.urlresolvers import reverse
15from django import http
16
17from mox3.mox import IsA
18
19from openstack_dashboard import api
20from openstack_dashboard.test import helpers as test
21
22
23class ResourceTypesTests(test.TestCase):
24
25 @test.create_stubs({api.heat: ('resource_types_list',)})
26 def test_index(self):
27 filters = {}
28 api.heat.resource_types_list(
29 IsA(http.HttpRequest), filters=filters).AndReturn(
30 self.resource_types.list())
31 self.mox.ReplayAll()
32
33 res = self.client.get(
34 reverse('horizon:project:stacks.resource_types:index'))
35 self.assertTemplateUsed(
36 res, 'horizon/common/_data_table_view.html')
37 self.assertContains(res, 'AWS::CloudFormation::Stack')
38
39 @test.create_stubs({api.heat: ('resource_type_get',)})
40 def test_detail_view(self):
41 rt = self.api_resource_types.first()
42
43 api.heat.resource_type_get(
44 IsA(http.HttpRequest), rt['resource_type']).AndReturn(rt)
45 self.mox.ReplayAll()
46
47 url = reverse('horizon:project:stacks.resource_types:details',
48 args=[rt['resource_type']])
49 res = self.client.get(url)
50
51 self.assertTemplateUsed(res, 'horizon/common/_detail.html')
52 self.assertNoMessages()
diff --git a/openstack_dashboard/dashboards/project/stacks/resource_types/urls.py b/openstack_dashboard/dashboards/project/stacks/resource_types/urls.py
deleted file mode 100644
index 8ab8cb0..0000000
--- a/openstack_dashboard/dashboards/project/stacks/resource_types/urls.py
+++ /dev/null
@@ -1,22 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10# implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14from django.conf.urls import url
15
16from openstack_dashboard.dashboards.project.stacks.resource_types import views
17
18urlpatterns = [
19 url(r'^$', views.ResourceTypesView.as_view(), name='index'),
20 url(r'^(?P<resource_type>[^/]+)/$',
21 views.DetailView.as_view(), name='details'),
22]
diff --git a/openstack_dashboard/dashboards/project/stacks/resource_types/views.py b/openstack_dashboard/dashboards/project/stacks/resource_types/views.py
deleted file mode 100644
index 8afcdf8..0000000
--- a/openstack_dashboard/dashboards/project/stacks/resource_types/views.py
+++ /dev/null
@@ -1,78 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10# implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14import yaml
15
16from django.core.urlresolvers import reverse
17from django.utils.translation import ugettext_lazy as _
18
19from horizon import exceptions
20from horizon import tables
21from horizon import tabs
22
23from openstack_dashboard import api
24import openstack_dashboard.dashboards.project.stacks.resource_types.tables \
25 as project_tables
26import openstack_dashboard.dashboards.project.stacks.resource_types.tabs \
27 as project_tabs
28
29
30class ResourceTypesView(tables.DataTableView):
31 table_class = project_tables.ResourceTypesTable
32 page_title = _("Resource Types")
33
34 def get_data(self):
35 try:
36 filters = self.get_filters()
37 if 'name' in filters:
38 filters['name'] = '.*' + filters['name']
39 r_types = sorted(api.heat.resource_types_list(self.request,
40 filters=filters),
41 key=lambda resource: resource.resource_type)
42 except Exception:
43 r_types = []
44 msg = _('Unable to retrieve stack resource types.')
45 exceptions.handle(self.request, msg)
46 return r_types
47
48
49class DetailView(tabs.TabView):
50 tab_group_class = project_tabs.ResourceTypeDetailsTabs
51 template_name = 'horizon/common/_detail.html'
52 page_title = "{{ resource_type }}"
53
54 def get_resource_type(self, request, **kwargs):
55 try:
56 resource_type_overview = api.heat.resource_type_get(
57 request,
58 kwargs['resource_type'])
59 return resource_type_overview
60 except Exception:
61 msg = _('Unable to retrieve resource type details.')
62 exceptions.handle(request, msg, redirect=self.get_redirect_url())
63
64 def get_tabs(self, request, **kwargs):
65 resource_type_overview = self.get_resource_type(request, **kwargs)
66 r_type = resource_type_overview['resource_type']
67 r_type_attributes = resource_type_overview['attributes']
68 r_type_properties = resource_type_overview['properties']
69 return self.tab_group_class(
70 request,
71 rt=r_type,
72 rt_attributes=yaml.safe_dump(r_type_attributes, indent=2),
73 rt_properties=yaml.safe_dump(r_type_properties, indent=2),
74 **kwargs)
75
76 @staticmethod
77 def get_redirect_url():
78 return reverse('horizon:project:stacks.resources:index')
diff --git a/openstack_dashboard/dashboards/project/stacks/sro.py b/openstack_dashboard/dashboards/project/stacks/sro.py
deleted file mode 100644
index 4250e7e..0000000
--- a/openstack_dashboard/dashboards/project/stacks/sro.py
+++ /dev/null
@@ -1,44 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13from django.template.defaultfilters import title
14from django.template.loader import render_to_string
15
16from horizon.utils import filters
17
18
19def stack_info(stack, stack_image):
20 stack.stack_status_desc = title(
21 filters.replace_underscores(stack.stack_status))
22 if stack.stack_status_reason:
23 stack.stack_status_reason = title(
24 filters.replace_underscores(stack.stack_status_reason)
25 )
26 context = {}
27 context['stack'] = stack
28 context['stack_image'] = stack_image
29 return render_to_string('project/stacks/_stack_info.html',
30 context)
31
32
33def resource_info(resource):
34 resource.resource_status_desc = title(
35 filters.replace_underscores(resource.resource_status)
36 )
37 if resource.resource_status_reason:
38 resource.resource_status_reason = title(
39 filters.replace_underscores(resource.resource_status_reason)
40 )
41 context = {}
42 context['resource'] = resource
43 return render_to_string('project/stacks/_resource_info.html',
44 context)
diff --git a/openstack_dashboard/dashboards/project/stacks/tables.py b/openstack_dashboard/dashboards/project/stacks/tables.py
deleted file mode 100644
index 96391ab..0000000
--- a/openstack_dashboard/dashboards/project/stacks/tables.py
+++ /dev/null
@@ -1,413 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13from django.core import urlresolvers
14from django.http import Http404
15from django.template.defaultfilters import title
16from django.utils.translation import pgettext_lazy
17from django.utils.translation import ugettext_lazy as _
18from django.utils.translation import ungettext_lazy
19from heatclient import exc
20
21from horizon import exceptions
22from horizon import messages
23from horizon import tables
24from horizon.utils import filters
25
26from openstack_dashboard import api
27from openstack_dashboard.dashboards.project.stacks import mappings
28
29
30class LaunchStack(tables.LinkAction):
31 name = "launch"
32 verbose_name = _("Launch Stack")
33 url = "horizon:project:stacks:select_template"
34 classes = ("ajax-modal",)
35 icon = "plus"
36 policy_rules = (("orchestration", "stacks:validate_template"),
37 ("orchestration", "stacks:create"),)
38
39
40class PreviewStack(tables.LinkAction):
41 name = "preview"
42 verbose_name = _("Preview Stack")
43 url = "horizon:project:stacks:preview_template"
44 classes = ("ajax-modal",)
45 icon = "eye"
46 policy_rules = (("orchestration", "stacks:validate_template"),
47 ("orchestration", "stacks:preview"),)
48
49
50class CheckStack(tables.BatchAction):
51 name = "check"
52 verbose_name = _("Check Stack")
53 policy_rules = (("orchestration", "actions:action"),)
54 icon = "check-square"
55
56 @staticmethod
57 def action_present(count):
58 return ungettext_lazy(
59 u"Check Stack",
60 u"Check Stacks",
61 count
62 )
63
64 @staticmethod
65 def action_past(count):
66 return ungettext_lazy(
67 u"Checked Stack",
68 u"Checked Stacks",
69 count
70 )
71
72 def action(self, request, stack_id):
73 api.heat.action_check(request, stack_id)
74
75
76class SuspendStack(tables.BatchAction):
77 name = "suspend"
78 verbose_name = _("Suspend Stack")
79 policy_rules = (("orchestration", "actions:action"),)
80 icon = "pause"
81
82 @staticmethod
83 def action_present(count):
84 return ungettext_lazy(
85 u"Suspend Stack",
86 u"Suspend Stacks",
87 count
88 )
89
90 @staticmethod
91 def action_past(count):
92 return ungettext_lazy(
93 u"Suspended Stack",
94 u"Suspended Stacks",
95 count
96 )
97
98 def action(self, request, stack_id):
99 try:
100 api.heat.action_suspend(request, stack_id)
101 except Exception:
102 msg = _('Failed to suspend stack.')
103 exceptions.handle(request, msg)
104
105
106class ResumeStack(tables.BatchAction):
107 name = "resume"
108 verbose_name = _("Resume Stack")
109 policy_rules = (("orchestration", "actions:action"),)
110 icon = "play"
111
112 @staticmethod
113 def action_present(count):
114 return ungettext_lazy(
115 u"Resume Stack",
116 u"Resume Stacks",
117 count
118 )
119
120 @staticmethod
121 def action_past(count):
122 return ungettext_lazy(
123 u"Resumed Stack",
124 u"Resumed Stacks",
125 count
126 )
127
128 def action(self, request, stack_id):
129 try:
130 api.heat.action_resume(request, stack_id)
131 except Exception:
132 msg = _('Failed to resume stack.')
133 exceptions.handle(request, msg)
134
135
136class ChangeStackTemplate(tables.LinkAction):
137 name = "edit"
138 verbose_name = _("Change Stack Template")
139 url = "horizon:project:stacks:change_template"
140 classes = ("ajax-modal",)
141 icon = "pencil"
142
143 def get_link_url(self, stack):
144 return urlresolvers.reverse(self.url, args=[stack.id])
145
146
147class DeleteStack(tables.DeleteAction):
148 @staticmethod
149 def action_present(count):
150 return ungettext_lazy(
151 u"Delete Stack",
152 u"Delete Stacks",
153 count
154 )
155
156 @staticmethod
157 def action_past(count):
158 return ungettext_lazy(
159 u"Deleted Stack",
160 u"Deleted Stacks",
161 count
162 )
163
164 policy_rules = (("orchestration", "stacks:delete"),)
165
166 def delete(self, request, stack_id):
167 try:
168 api.heat.stack_delete(request, stack_id)
169 except Exception:
170 msg = _('Failed to delete stack.')
171 exceptions.handle(request, msg)
172
173 def allowed(self, request, stack):
174 if stack is not None:
175 return stack.stack_status != 'DELETE_COMPLETE'
176 return True
177
178
179class StacksUpdateRow(tables.Row):
180 ajax = True
181
182 def can_be_selected(self, datum):
183 return datum.stack_status != 'DELETE_COMPLETE'
184
185 def get_data(self, request, stack_id):
186 try:
187 stack = api.heat.stack_get(request, stack_id)
188 if stack.stack_status == 'DELETE_COMPLETE':
189 # returning 404 to the ajax call removes the
190 # row from the table on the ui
191 raise Http404
192 return stack
193 except Http404:
194 raise
195 except Exception as e:
196 messages.error(request, e)
197 raise
198
199
200class StacksFilterAction(tables.FilterAction):
201 filter_type = 'server'
202 filter_choices = (('name', _('Stack Name ='), True, _('Case-sensitive')),
203 ('id', _('Stack ID ='), True),
204 ('status', _('Status ='), True))
205
206
207class StacksTable(tables.DataTable):
208 STATUS_CHOICES = (
209 ("Complete", True),
210 ("Failed", False),
211 )
212 STACK_STATUS_DISPLAY_CHOICES = (
213 ("init_in_progress", pgettext_lazy("current status of stack",
214 u"Init In Progress")),
215 ("init_complete", pgettext_lazy("current status of stack",
216 u"Init Complete")),
217 ("init_failed", pgettext_lazy("current status of stack",
218 u"Init Failed")),
219 ("create_in_progress", pgettext_lazy("current status of stack",
220 u"Create In Progress")),
221 ("create_complete", pgettext_lazy("current status of stack",
222 u"Create Complete")),
223 ("create_failed", pgettext_lazy("current status of stack",
224 u"Create Failed")),
225 ("delete_in_progress", pgettext_lazy("current status of stack",
226 u"Delete In Progress")),
227 ("delete_complete", pgettext_lazy("current status of stack",
228 u"Delete Complete")),
229 ("delete_failed", pgettext_lazy("current status of stack",
230 u"Delete Failed")),
231 ("update_in_progress", pgettext_lazy("current status of stack",
232 u"Update In Progress")),
233 ("update_complete", pgettext_lazy("current status of stack",
234 u"Update Complete")),
235 ("update_failed", pgettext_lazy("current status of stack",
236 u"Update Failed")),
237 ("rollback_in_progress", pgettext_lazy("current status of stack",
238 u"Rollback In Progress")),
239 ("rollback_complete", pgettext_lazy("current status of stack",
240 u"Rollback Complete")),
241 ("rollback_failed", pgettext_lazy("current status of stack",
242 u"Rollback Failed")),
243 ("suspend_in_progress", pgettext_lazy("current status of stack",
244 u"Suspend In Progress")),
245 ("suspend_complete", pgettext_lazy("current status of stack",
246 u"Suspend Complete")),
247 ("suspend_failed", pgettext_lazy("current status of stack",
248 u"Suspend Failed")),
249 ("resume_in_progress", pgettext_lazy("current status of stack",
250 u"Resume In Progress")),
251 ("resume_complete", pgettext_lazy("current status of stack",
252 u"Resume Complete")),
253 ("resume_failed", pgettext_lazy("current status of stack",
254 u"Resume Failed")),
255 ("adopt_in_progress", pgettext_lazy("current status of stack",
256 u"Adopt In Progress")),
257 ("adopt_complete", pgettext_lazy("current status of stack",
258 u"Adopt Complete")),
259 ("adopt_failed", pgettext_lazy("current status of stack",
260 u"Adopt Failed")),
261 ("snapshot_in_progress", pgettext_lazy("current status of stack",
262 u"Snapshot In Progress")),
263 ("snapshot_complete", pgettext_lazy("current status of stack",
264 u"Snapshot Complete")),
265 ("snapshot_failed", pgettext_lazy("current status of stack",
266 u"Snapshot Failed")),
267 ("check_in_progress", pgettext_lazy("current status of stack",
268 u"Check In Progress")),
269 ("check_complete", pgettext_lazy("current status of stack",
270 u"Check Complete")),
271 ("check_failed", pgettext_lazy("current status of stack",
272 u"Check Failed")),
273 )
274 name = tables.Column("stack_name",
275 verbose_name=_("Stack Name"),
276 link="horizon:project:stacks:detail",)
277 created = tables.Column("creation_time",
278 verbose_name=_("Created"),
279 filters=(filters.parse_isotime,
280 filters.timesince_sortable),
281 attrs={'data-type': 'timesince'})
282 updated = tables.Column("updated_time",
283 verbose_name=_("Updated"),
284 filters=(filters.parse_isotime,
285 filters.timesince_or_never))
286 status = tables.Column("status",
287 hidden=True,
288 status=True,
289 status_choices=STATUS_CHOICES)
290
291 stack_status = tables.Column("stack_status",
292 verbose_name=_("Status"),
293 display_choices=STACK_STATUS_DISPLAY_CHOICES)
294
295 def get_object_display(self, stack):
296 return stack.stack_name
297
298 class Meta(object):
299 name = "stacks"
300 verbose_name = _("Stacks")
301 pagination_param = 'stack_marker'
302 status_columns = ["status", ]
303 row_class = StacksUpdateRow
304 table_actions_menu = (CheckStack,
305 SuspendStack,
306 ResumeStack,)
307 table_actions = (LaunchStack,
308 PreviewStack,
309 DeleteStack,
310 StacksFilterAction,)
311 row_actions = (CheckStack,
312 SuspendStack,
313 ResumeStack,
314 ChangeStackTemplate,
315 DeleteStack,)
316
317
318def get_resource_url(obj):
319 if obj.physical_resource_id == obj.stack_id:
320 return None
321 return urlresolvers.reverse('horizon:project:stacks:resource',
322 args=(obj.stack_id, obj.resource_name))
323
324
325class EventsTable(tables.DataTable):
326
327 logical_resource = tables.Column('resource_name',
328 verbose_name=_("Stack Resource"),
329 link=get_resource_url)
330 physical_resource = tables.Column('physical_resource_id',
331 verbose_name=_("Resource"))
332 timestamp = tables.Column('event_time',
333 verbose_name=_("Time Since Event"),
334 filters=(filters.parse_isotime,
335 filters.timesince_or_never))
336 status = tables.Column("resource_status",
337 filters=(title, filters.replace_underscores),
338 verbose_name=_("Status"),)
339
340 statusreason = tables.Column("resource_status_reason",
341 verbose_name=_("Status Reason"),)
342
343 class Meta(object):
344 name = "events"
345 verbose_name = _("Stack Events")
346
347
348class ResourcesUpdateRow(tables.Row):
349 ajax = True
350
351 def get_data(self, request, resource_name):
352 try:
353 stack = self.table.stack
354 stack_identifier = '%s/%s' % (stack.stack_name, stack.id)
355 return api.heat.resource_get(
356 request, stack_identifier, resource_name)
357 except exc.HTTPNotFound:
358 # returning 404 to the ajax call removes the
359 # row from the table on the ui
360 raise Http404
361 except Exception as e:
362 messages.error(request, e)
363
364
365class ResourcesTable(tables.DataTable):
366 class StatusColumn(tables.Column):
367 def get_raw_data(self, datum):
368 return datum.resource_status.partition("_")[2]
369
370 STATUS_CHOICES = (
371 ("Complete", True),
372 ("Failed", False),
373 )
374 STATUS_DISPLAY_CHOICES = StacksTable.STACK_STATUS_DISPLAY_CHOICES
375
376 logical_resource = tables.Column('resource_name',
377 verbose_name=_("Stack Resource"),
378 link=get_resource_url)
379 physical_resource = tables.Column('physical_resource_id',
380 verbose_name=_("Resource"),
381 link=mappings.resource_to_url)
382 resource_type = tables.Column("resource_type",
383 verbose_name=_("Stack Resource Type"),)
384 updated_time = tables.Column('updated_time',
385 verbose_name=_("Date Updated"),
386 filters=(filters.parse_isotime,
387 filters.timesince_or_never))
388 status = tables.Column("resource_status",
389 verbose_name=_("Status"),
390 display_choices=STATUS_DISPLAY_CHOICES)
391
392 statusreason = tables.Column("resource_status_reason",
393 verbose_name=_("Status Reason"),)
394
395 status_hidden = StatusColumn("status",
396 hidden=True,
397 status=True,
398 status_choices=STATUS_CHOICES)
399
400 def __init__(self, request, data=None,
401 needs_form_wrapper=None, **kwargs):
402 super(ResourcesTable, self).__init__(
403 request, data, needs_form_wrapper, **kwargs)
404 self.stack = kwargs['stack']
405
406 def get_object_id(self, datum):
407 return datum.resource_name
408
409 class Meta(object):
410 name = "resources"
411 verbose_name = _("Stack Resources")
412 status_columns = ["status_hidden", ]
413 row_class = ResourcesUpdateRow
diff --git a/openstack_dashboard/dashboards/project/stacks/tabs.py b/openstack_dashboard/dashboards/project/stacks/tabs.py
deleted file mode 100644
index 1e6c92b..0000000
--- a/openstack_dashboard/dashboards/project/stacks/tabs.py
+++ /dev/null
@@ -1,173 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import logging
14
15from django.utils.translation import ugettext_lazy as _
16
17from horizon import messages
18from horizon import tabs
19from openstack_dashboard import api
20from openstack_dashboard import policy
21
22from openstack_dashboard.dashboards.project.stacks \
23 import api as project_api
24from openstack_dashboard.dashboards.project.stacks import mappings
25from openstack_dashboard.dashboards.project.stacks \
26 import tables as project_tables
27
28
29LOG = logging.getLogger(__name__)
30
31
32class StackTopologyTab(tabs.Tab):
33 name = _("Topology")
34 slug = "topology"
35 template_name = "project/stacks/_detail_topology.html"
36 preload = False
37
38 def allowed(self, request):
39 return policy.check(
40 (("orchestration", "stacks:template"),
41 ("orchestration", "stacks:lookup"),
42 ("orchestration", "stacks:show"),
43 ("orchestration", "resource:index"),),
44 request)
45
46 def get_context_data(self, request):
47 context = {}
48 stack = self.tab_group.kwargs['stack']
49 context['stack_id'] = stack.id
50 context['d3_data'] = project_api.d3_data(request, stack_id=stack.id)
51 return context
52
53
54class StackOverviewTab(tabs.Tab):
55 name = _("Overview")
56 slug = "overview"
57 template_name = "project/stacks/_detail_overview.html"
58
59 def allowed(self, request):
60 return policy.check(
61 (("orchestration", "stacks:template"),
62 ("orchestration", "stacks:lookup"),
63 ("orchestration", "stacks:show"),),
64 request)
65
66 def get_context_data(self, request):
67 return {"stack": self.tab_group.kwargs['stack']}
68
69
70class ResourceOverviewTab(tabs.Tab):
71 name = _("Overview")
72 slug = "resource_overview"
73 template_name = "project/stacks/_resource_overview.html"
74
75 def get_context_data(self, request):
76 resource = self.tab_group.kwargs['resource']
77 resource_url = mappings.resource_to_url(resource)
78 return {
79 "resource": resource,
80 "resource_url": resource_url,
81 "metadata": self.tab_group.kwargs['metadata']}
82
83
84class StackEventsTab(tabs.Tab):
85 name = _("Events")
86 slug = "events"
87 template_name = "project/stacks/_detail_events.html"
88 preload = False
89
90 def allowed(self, request):
91 return policy.check(
92 (("orchestration", "stacks:template"),
93 ("orchestration", "stacks:lookup"),
94 ("orchestration", "stacks:show"),
95 ("orchestration", "events:index"),),
96 request)
97
98 def get_context_data(self, request):
99 stack = self.tab_group.kwargs['stack']
100 try:
101 stack_identifier = '%s/%s' % (stack.stack_name, stack.id)
102 events = api.heat.events_list(self.request, stack_identifier)
103 LOG.debug('got events %s', events)
104 # The stack id is needed to generate the resource URL.
105 for event in events:
106 event.stack_id = stack.id
107 except Exception:
108 events = []
109 messages.error(request, _(
110 'Unable to get events for stack "%s".') % stack.stack_name)
111 return {"stack": stack,
112 "table": project_tables.EventsTable(request, data=events), }
113
114
115class StackResourcesTab(tabs.Tab):
116 name = _("Resources")
117 slug = "resources"
118 template_name = "project/stacks/_detail_resources.html"
119 preload = False
120
121 def allowed(self, request):
122 return policy.check(
123 (("orchestration", "stacks:template"),
124 ("orchestration", "stacks:lookup"),
125 ("orchestration", "stacks:show"),
126 ("orchestration", "resource:index"),),
127 request)
128
129 def get_context_data(self, request):
130 stack = self.tab_group.kwargs['stack']
131 try:
132 stack_identifier = '%s/%s' % (stack.stack_name, stack.id)
133 resources = api.heat.resources_list(self.request, stack_identifier)
134 LOG.debug('got resources %s', resources)
135 # The stack id is needed to generate the resource URL.
136 for r in resources:
137 r.stack_id = stack.id
138 except Exception:
139 resources = []
140 messages.error(request, _(
141 'Unable to get resources for stack "%s".') % stack.stack_name)
142 return {"stack": stack,
143 "table": project_tables.ResourcesTable(
144 request, data=resources, stack=stack), }
145
146
147class StackTemplateTab(tabs.Tab):
148 name = _("Template")
149 slug = "stack_template"
150 template_name = "project/stacks/_stack_template.html"
151
152 def allowed(self, request):
153 return policy.check(
154 (("orchestration", "stacks:template"),
155 ("orchestration", "stacks:lookup"),
156 ("orchestration", "stacks:show"),),
157 request)
158
159 def get_context_data(self, request):
160 return {"stack_template": self.tab_group.kwargs['stack_template']}
161
162
163class StackDetailTabs(tabs.TabGroup):
164 slug = "stack_details"
165 tabs = (StackTopologyTab, StackOverviewTab, StackResourcesTab,
166 StackEventsTab, StackTemplateTab)
167 sticky = True
168
169
170class ResourceDetailTabs(tabs.TabGroup):
171 slug = "resource_details"
172 tabs = (ResourceOverviewTab,)
173 sticky = True
diff --git a/openstack_dashboard/dashboards/project/stacks/template_versions/__init__.py b/openstack_dashboard/dashboards/project/stacks/template_versions/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/openstack_dashboard/dashboards/project/stacks/template_versions/__init__.py
+++ /dev/null
diff --git a/openstack_dashboard/dashboards/project/stacks/template_versions/panel.py b/openstack_dashboard/dashboards/project/stacks/template_versions/panel.py
deleted file mode 100644
index 3492d59..0000000
--- a/openstack_dashboard/dashboards/project/stacks/template_versions/panel.py
+++ /dev/null
@@ -1,23 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10# implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14from django.utils.translation import ugettext_lazy as _
15
16import horizon
17
18
19class TemplateVersions(horizon.Panel):
20 name = _("Template Versions")
21 slug = "stacks.template_versions"
22 permissions = ('openstack.services.orchestration',)
23 policy_rules = (("orchestration", "stacks:list_template_versions"),)
diff --git a/openstack_dashboard/dashboards/project/stacks/template_versions/tables.py b/openstack_dashboard/dashboards/project/stacks/template_versions/tables.py
deleted file mode 100644
index 8c63d14..0000000
--- a/openstack_dashboard/dashboards/project/stacks/template_versions/tables.py
+++ /dev/null
@@ -1,52 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10# implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14from django.template import defaultfilters as filters
15from django.utils.translation import ugettext_lazy as _
16
17from horizon import tables
18
19
20class TemplateVersionsTable(tables.DataTable):
21 version = tables.Column(
22 "version",
23 verbose_name=_("Version"),
24 link="horizon:project:stacks.template_versions:details",)
25 type = tables.Column(
26 "type",
27 verbose_name=_("Type"),
28 filters=(filters.upper,))
29
30 def get_object_id(self, template_versions):
31 return template_versions.version
32
33 class Meta(object):
34 name = "template_versions"
35 table_actions = (tables.FilterAction,)
36 verbose_name = _("Template Versions")
37 table_actions = (tables.FilterAction,)
38 multi_select = False
39
40
41class TemplateFunctionsTable(tables.DataTable):
42 functions = tables.Column('functions', verbose_name=_("Function"))
43 description = tables.Column('description', verbose_name=_("Description"))
44
45 def get_object_id(self, template_functions):
46 return template_functions.functions
47
48 class Meta(object):
49 name = "template_functions"
50 verbose_name = _("Template Functions")
51 table_actions = (tables.FilterAction,)
52 multi_select = False
diff --git a/openstack_dashboard/dashboards/project/stacks/template_versions/tabs.py b/openstack_dashboard/dashboards/project/stacks/template_versions/tabs.py
deleted file mode 100644
index 9cb9096..0000000
--- a/openstack_dashboard/dashboards/project/stacks/template_versions/tabs.py
+++ /dev/null
@@ -1,51 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10# implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14from django.utils.translation import ugettext_lazy as _
15
16from horizon import messages
17from horizon import tabs
18from openstack_dashboard import api
19from openstack_dashboard import policy
20
21from openstack_dashboard.dashboards.project.stacks.template_versions \
22 import tables as project_tables
23
24
25class TemplateFunctionsTab(tabs.Tab):
26 name = _("Template Functions")
27 slug = "template_functions"
28 template_name = "project/stacks.template_versions/_details.html"
29 preload = False
30
31 def allowed(self, request):
32 return policy.check(
33 (("orchestration", "stacks:list_template_functions"),),
34 request)
35
36 def get_context_data(self, request):
37 template_version = self.tab_group.kwargs['template_version']
38 try:
39 template_functions = api.heat.template_function_list(
40 self.request, template_version)
41 except Exception:
42 template_functions = []
43 messages.error(request, _('Unable to get functions for template '
44 'version "%s".') % template_version)
45 return {"table": project_tables.TemplateFunctionsTable(
46 request, data=template_functions), }
47
48
49class TemplateVersionDetailsTabs(tabs.TabGroup):
50 slug = "template_version_details"
51 tabs = (TemplateFunctionsTab,)
diff --git a/openstack_dashboard/dashboards/project/stacks/template_versions/templates/stacks.template_versions/_details.html b/openstack_dashboard/dashboards/project/stacks/template_versions/templates/stacks.template_versions/_details.html
deleted file mode 100644
index 9976f88..0000000
--- a/openstack_dashboard/dashboards/project/stacks/template_versions/templates/stacks.template_versions/_details.html
+++ /dev/null
@@ -1,3 +0,0 @@
1{% load i18n %}
2
3{{ table.render }}
diff --git a/openstack_dashboard/dashboards/project/stacks/template_versions/templates/stacks.template_versions/index.html b/openstack_dashboard/dashboards/project/stacks/template_versions/templates/stacks.template_versions/index.html
deleted file mode 100644
index 0a39d4a..0000000
--- a/openstack_dashboard/dashboards/project/stacks/template_versions/templates/stacks.template_versions/index.html
+++ /dev/null
@@ -1,7 +0,0 @@
1{% extends 'base.html' %}
2{% load i18n %}
3{% block title %}{% trans "Template Versions" %}{% endblock %}
4
5{% block main %}
6 {{ table.render }}
7{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/template_versions/tests.py b/openstack_dashboard/dashboards/project/stacks/template_versions/tests.py
deleted file mode 100644
index fcace15..0000000
--- a/openstack_dashboard/dashboards/project/stacks/template_versions/tests.py
+++ /dev/null
@@ -1,79 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10# implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14from django.core.urlresolvers import reverse
15from django import http
16
17from mox3.mox import IsA
18
19from openstack_dashboard import api
20from openstack_dashboard.test import helpers as test
21
22
23class TemplateVersionsTests(test.TestCase):
24 INDEX_URL = reverse('horizon:project:stacks.template_versions:index')
25
26 @test.create_stubs({api.heat: ('template_version_list',)})
27 def test_index(self):
28 api.heat.template_version_list(
29 IsA(http.HttpRequest)).AndReturn(self.template_versions.list())
30 self.mox.ReplayAll()
31
32 res = self.client.get(self.INDEX_URL)
33 self.assertTemplateUsed(
34 res, 'project/stacks.template_versions/index.html')
35 self.assertContains(res, 'HeatTemplateFormatVersion.2012-12-12')
36
37 @test.create_stubs({api.heat: ('template_version_list',)})
38 def test_index_exception(self):
39 api.heat.template_version_list(
40 IsA(http.HttpRequest)).AndRaise(self.exceptions.heat)
41 self.mox.ReplayAll()
42
43 res = self.client.get(self.INDEX_URL)
44 self.assertTemplateUsed(
45 res, 'project/stacks.template_versions/index.html')
46 self.assertEqual(len(res.context['table'].data), 0)
47 self.assertMessageCount(res, error=1)
48
49 @test.create_stubs({api.heat: ('template_function_list',)})
50 def test_detail_view(self):
51 t_version = self.template_versions.first().version
52 t_functions = self.template_functions.list()
53
54 api.heat.template_function_list(
55 IsA(http.HttpRequest), t_version).AndReturn(t_functions)
56 self.mox.ReplayAll()
57
58 url = reverse('horizon:project:stacks.template_versions:details',
59 args=[t_version])
60 res = self.client.get(url)
61
62 self.assertTemplateUsed(res, 'horizon/common/_detail.html')
63 self.assertNoMessages()
64
65 @test.create_stubs({api.heat: ('template_function_list',)})
66 def test_detail_view_with_exception(self):
67 t_version = self.template_versions.first().version
68
69 api.heat.template_function_list(
70 IsA(http.HttpRequest), t_version).AndRaise(self.exceptions.heat)
71 self.mox.ReplayAll()
72
73 url = reverse('horizon:project:stacks.template_versions:details',
74 args=[t_version])
75 res = self.client.get(url)
76
77 self.assertTemplateUsed(res, 'horizon/common/_detail.html')
78 self.assertEqual(len(res.context['table'].data), 0)
79 self.assertMessageCount(res, error=1)
diff --git a/openstack_dashboard/dashboards/project/stacks/template_versions/urls.py b/openstack_dashboard/dashboards/project/stacks/template_versions/urls.py
deleted file mode 100644
index 5e2bbc2..0000000
--- a/openstack_dashboard/dashboards/project/stacks/template_versions/urls.py
+++ /dev/null
@@ -1,24 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10# implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14from django.conf.urls import url
15
16from openstack_dashboard.dashboards.project.stacks.template_versions \
17 import views
18
19
20urlpatterns = [
21 url(r'^$', views.TemplateVersionsView.as_view(), name='index'),
22 url(r'^(?P<template_version>[^/]+)/$',
23 views.DetailView.as_view(), name='details'),
24]
diff --git a/openstack_dashboard/dashboards/project/stacks/template_versions/views.py b/openstack_dashboard/dashboards/project/stacks/template_versions/views.py
deleted file mode 100644
index 22eabd7..0000000
--- a/openstack_dashboard/dashboards/project/stacks/template_versions/views.py
+++ /dev/null
@@ -1,61 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10# implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14from django.core.urlresolvers import reverse
15from django.utils.translation import ugettext_lazy as _
16
17from horizon import exceptions
18from horizon import tables
19from horizon import tabs
20
21from openstack_dashboard import api
22import openstack_dashboard.dashboards.project.stacks.template_versions.tables \
23 as project_tables
24import openstack_dashboard.dashboards.project.stacks.template_versions.tabs \
25 as project_tabs
26
27
28class TemplateVersionsView(tables.DataTableView):
29 table_class = project_tables.TemplateVersionsTable
30 template_name = 'project/stacks.template_versions/index.html'
31 page_title = _("Template Versions")
32
33 def get_data(self):
34 try:
35 template_versions = sorted(
36 api.heat.template_version_list(self.request),
37 key=lambda template_version: template_version.version)
38 except Exception:
39 template_versions = []
40 msg = _('Unable to retrieve template versions.')
41 exceptions.handle(self.request, msg)
42 return template_versions
43
44
45class DetailView(tabs.TabView):
46 tab_group_class = project_tabs.TemplateVersionDetailsTabs
47 template_name = 'horizon/common/_detail.html'
48 page_title = "{{ template_version }}"
49
50 def get_template_version(self, request, **kwargs):
51 try:
52 template_functions = api.heat.template_function_list(
53 request, kwargs['template_version'])
54 return template_functions
55 except Exception:
56 msg = _('Unable to retrieve template functions.')
57 exceptions.handle(request, msg, redirect=self.get_redirect_url())
58
59 @staticmethod
60 def get_redirect_url():
61 return reverse('horizon:project:stacks.template_versions:index')
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_change_template.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_change_template.html
deleted file mode 100644
index bd31f70..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_change_template.html
+++ /dev/null
@@ -1,7 +0,0 @@
1{% extends "horizon/common/_modal_form.html" %}
2{% load i18n %}
3{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
4{% block modal-body-right %}
5 <h3>{% trans "Description:" %}</h3>
6 <p>{% trans "Use one of the available template source options to specify the template to be used in creating this stack." %}</p>
7{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_create.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_create.html
deleted file mode 100644
index 19a3f1b..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_create.html
+++ /dev/null
@@ -1,6 +0,0 @@
1{% extends "horizon/common/_modal_form.html" %}
2{% load i18n %}
3{% block modal-body-right %}
4 <h3>{% trans "Description:" %}</h3>
5 <p>{% trans "Create a new stack with the provided values." %}</p>
6{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_events.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_events.html
deleted file mode 100644
index 9976f88..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_events.html
+++ /dev/null
@@ -1,3 +0,0 @@
1{% load i18n %}
2
3{{ table.render }}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_overview.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_overview.html
deleted file mode 100644
index 56534bb..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_overview.html
+++ /dev/null
@@ -1,55 +0,0 @@
1{% load i18n sizeformat %}
2
3<div class="detail">
4 <dl class="dl-horizontal">
5 <dt>{% trans "Name" %}</dt>
6 <dd data-display="{{ stack.stack_name|default:stack.id }}">{{ stack.stack_name }}</dd>
7 <dt>{% trans "ID" %}</dt>
8 <dd>{{ stack.id }}</dd>
9 <dt>{% trans "Description" %}</dt>
10 <dd>{{ stack.description }}</dd>
11 </dl>
12
13 <h4>{% trans "Status" %}</h4>
14 <hr class="header_rule">
15 <dl class="dl-horizontal">
16 <dt>{% trans "Created" %}</dt>
17 <dd>{{ stack.creation_time|parse_isotime|timesince_or_never }}</dd>
18 <dt>{% trans "Last Updated" %}</dt>
19 <dd>{{ stack.updated_time|parse_isotime|timesince_or_never }}</dd>
20 <dt>{% trans "Status" %}</dt>
21 <dd>
22 {% blocktrans with stack_status_title=stack.stack_status|title stack_status_reason=stack.stack_status_reason %}{{ stack_status_title }}: {{ stack_status_reason }}{% endblocktrans %}
23 </dd>
24 </dl>
25
26 <h4>{% trans "Outputs" %}</h4>
27 <hr class="header_rule">
28 <dl class="dl-horizontal">
29 {% for output in stack.outputs %}
30 <dt>{{ output.output_key }}</dt>
31 <dd>{{ output.description }}</dd>
32 <dd>
33 {{ output.output_value|stack_output }}
34 </dd>
35 {% endfor %}
36 </dl>
37
38 <h4>{% trans "Stack Parameters" %}</h4>
39 <hr class="header_rule">
40 <dl class="dl-horizontal">
41 {% for key, value in stack.parameters.items %}
42 <dt>{{ key }}</dt>
43 <dd>{{ value }}</dd>
44 {% endfor %}
45 </dl>
46
47 <h4>{% trans "Launch Parameters" %}</h4>
48 <hr class="header_rule">
49 <dl class="dl-horizontal">
50 <dt>{% trans "Timeout" %}</dt>
51 <dd>{{ stack.timeout_mins }} {% trans "Minutes" %}</dd>
52 <dt>{% trans "Rollback" %}</dt>
53 <dd>{% if stack.disable_rollback %}{% trans "Disabled" %}{% else %}{% trans "Enabled" %}{% endif %}</dd>
54 </dl>
55</div>
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_resources.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_resources.html
deleted file mode 100644
index 9976f88..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_resources.html
+++ /dev/null
@@ -1,3 +0,0 @@
1{% load i18n %}
2
3{{ table.render }}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_topology.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_topology.html
deleted file mode 100644
index d906cea..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_detail_topology.html
+++ /dev/null
@@ -1,9 +0,0 @@
1{% load i18n sizeformat %}
2
3<div id="resource_container">
4 <div id="info_box"></div>
5 <div id="stack_box"></div>
6 <div id="heat_resource_topology"></div>
7 <div id="stack_id" data-stack_id="{{ stack_id }}"></div>
8 <div id="d3_data" data-d3_data="{{ d3_data }}"></div>
9</div> \ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview.html
deleted file mode 100644
index 2402478..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview.html
+++ /dev/null
@@ -1,6 +0,0 @@
1{% extends "horizon/common/_modal_form.html" %}
2{% load i18n %}
3{% block modal-body-right %}
4 <h3>{% trans "Description:" %}</h3>
5 <p>{% trans "Preview a new stack with the provided values." %}</p>
6{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_details.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_details.html
deleted file mode 100644
index 12d131a..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_details.html
+++ /dev/null
@@ -1,58 +0,0 @@
1{% extends "horizon/common/_modal.html" %}
2{% load i18n %}
3
4{% block modal-header %}{% trans "Stack Preview" %}{% endblock %}
5
6{% block modal-body %}
7<div class="row-fluid stack-preview detail">
8<form>
9 <dl class="dl-horizontal">
10 {% for key, value in stack_preview.items %}
11 {% if key != 'parameters' and key != 'resources' and key != 'links' %}
12 <dt>{{ key }}</dt>
13 <dd>{{ value }}</dd>
14 {% endif %}
15 {% endfor %}
16 </dl>
17
18 {% if stack_preview.parameters %}
19 <dt>{% trans "Parameters" %}</dt>
20 <hr class="header_rule">
21 <dl class="dl-horizontal">
22 {% for key, value in stack_preview.parameters.items %}
23 <dt>{{ key }}</dt>
24 <dd>{{ value }}</dd>
25 {% endfor %}
26 </dl>
27 {% endif %}
28
29 {% if stack_preview.links %}
30 <dt>{% trans "Links" %}</dt>
31 <hr class="header_rule">
32 {% for link in stack_preview.links %}
33 <dl class="dl-horizontal">
34 <dt>{{ link.rel }}</dt>
35 <dd>{{ link.href }}</dd>
36 </dl>
37 {% endfor %}
38 {% endif %}
39
40 {% if stack_preview.resources %}
41 <dt>{% trans "Resources" %}</dt>
42 {% for resource in stack_preview.resources %}
43 <hr class="header_rule">
44 <dl class="dl-horizontal">
45 {% for key, value in resource.items %}
46 <dt>{{ key }}</dt>
47 <dd>{{ value }}</dd>
48 {% endfor %}
49 </dl>
50 {% endfor %}
51 {% endif %}
52</form>
53</div>
54{% endblock %}
55
56{% block modal-footer %}
57 <a href="{% url 'horizon:project:stacks:index' %}" class="btn btn-default cancel">{% trans "Close" %}</a>
58{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_template.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_template.html
deleted file mode 100644
index 76f14b0..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_template.html
+++ /dev/null
@@ -1,7 +0,0 @@
1{% extends "horizon/common/_modal_form.html" %}
2{% load i18n %}
3{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
4{% block modal-body-right %}
5 <h3>{% trans "Description:" %}</h3>
6 <p>{% trans "Use one of the available template source options to specify the template to be used in previewing this stack." %}</p>
7{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_resource_info.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_resource_info.html
deleted file mode 100644
index 989c012..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_resource_info.html
+++ /dev/null
@@ -1,10 +0,0 @@
1<h3>{{ resource.resource_name }}</h3>
2
3{% if resource.resource_status == 'CREATE_FAILED' %}
4<p class="error">{{ resource.resource_status_desc }}</p>
5<p class="error">{{ resource.resource_status_reason }}</p>
6{% else %}
7<p>{{ resource.resource_status_desc }}</p>
8{% endif %}
9
10<p>{{ resource.resource_type }}</p> \ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_resource_overview.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_resource_overview.html
deleted file mode 100644
index ef1ca25..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_resource_overview.html
+++ /dev/null
@@ -1,38 +0,0 @@
1{% load i18n sizeformat %}
2
3<div class="detail">
4 <dl class="dl-horizontal">
5 <dt>{% trans "Stack Resource ID" %}</dt>
6 <dd>{{ resource.resource_name }}</dd>
7 <dt>{% trans "Resource ID" %}</dt>
8 <dd>
9 {% if resource_url %}
10 <a href="{{ resource_url }}">
11 {{ resource.physical_resource_id }}
12 </a>
13 {% else %}
14 {{ resource.physical_resource_id }}
15 {% endif %}
16 </dd>
17 <dt>{% trans "Stack Resource Type" %}</dt>
18 <dd>{{ resource.resource_type }}</dd>
19 <dt>{% trans "Description" %}</dt>
20 <dd>{{ resource.description }}</dd>
21 </dl>
22
23 <h4>{% trans "Status" %}</h4>
24 <hr class="header_rule">
25 <dl class="dl-horizontal">
26 <dt>{% trans "Last Updated" %}</dt>
27 <dd>{{ resource.updated_time|parse_isotime|timesince_or_never }}</dd>
28 <dt>{% trans "Status" %}</dt>
29 <dd>
30 {% blocktrans with resource_status=resource.resource_status|title|replace_underscores resource_status_reason=resource.resource_status_reason %}{{ resource_status }}: {{ resource_status_reason }}{% endblocktrans %}
31 </dd>
32 </dl>
33
34 <h4>{% trans "Resource Metadata" %}</h4>
35 <hr class="header_rule">
36 <pre>{{ metadata }}
37 </pre>
38</div>
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_select_template.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_select_template.html
deleted file mode 100644
index bd31f70..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_select_template.html
+++ /dev/null
@@ -1,7 +0,0 @@
1{% extends "horizon/common/_modal_form.html" %}
2{% load i18n %}
3{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
4{% block modal-body-right %}
5 <h3>{% trans "Description:" %}</h3>
6 <p>{% trans "Use one of the available template source options to specify the template to be used in creating this stack." %}</p>
7{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_stack_info.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_stack_info.html
deleted file mode 100644
index 6e657cb..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_stack_info.html
+++ /dev/null
@@ -1,14 +0,0 @@
1<img src="{{ stack_image }}" width="35px" height="35px" />
2<div id="stack_info">
3 <h3>{{ stack.stack_name }}</h3>
4 <p class="error">{{ stack.stack_status_desc }}</p>
5</div>
6<div class="clear"></div>
7{% if stack.stack_status == 'CREATE_FAILED' %}
8 <p class="error">{{ stack.stack_status_reason }}</p>
9{% endif %}
10{% for output in stack.outputs %}
11 {% if output.output_key == 'WebsiteURL' %}
12 <a href="{{ output.output_value }}">{{ output.description }}</a>
13 {% endif %}
14{% endfor %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_stack_template.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_stack_template.html
deleted file mode 100644
index 17eed20..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_stack_template.html
+++ /dev/null
@@ -1,5 +0,0 @@
1{% load i18n sizeformat %}
2
3<div class="detail">
4 <pre>{{ stack_template }}</pre>
5</div>
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_update.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_update.html
deleted file mode 100644
index f3b6337..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_update.html
+++ /dev/null
@@ -1,6 +0,0 @@
1{% extends "horizon/common/_modal_form.html" %}
2{% load i18n %}
3{% block modal-body-right %}
4 <h3>{% trans "Description:" %}</h3>
5 <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>
6{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/change_template.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/change_template.html
deleted file mode 100644
index 8bf6bfd..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/change_template.html
+++ /dev/null
@@ -1,7 +0,0 @@
1{% extends 'base.html' %}
2{% load i18n %}
3{% block title %}{% trans "Change Template" %}{% endblock %}
4
5{% block main %}
6 {% include 'project/stacks/_change_template.html' %}
7{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/create.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/create.html
deleted file mode 100644
index 7881a96..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/create.html
+++ /dev/null
@@ -1,7 +0,0 @@
1{% extends 'base.html' %}
2{% load i18n %}
3{% block title %}{% trans "Launch Stack" %}{% endblock %}
4
5{% block main %}
6 {% include 'project/stacks/_create.html' %}
7{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview.html
deleted file mode 100644
index ded74a4..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview.html
+++ /dev/null
@@ -1,7 +0,0 @@
1{% extends 'base.html' %}
2{% load i18n %}
3{% block title %}{% trans "Preview Stack" %}{% endblock %}
4
5{% block main %}
6 {% include 'project/stacks/_preview.html' %}
7{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_details.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_details.html
deleted file mode 100644
index 8d86df5..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_details.html
+++ /dev/null
@@ -1,7 +0,0 @@
1{% extends 'base.html' %}
2{% load i18n %}
3{% block title %}{% trans "Preview Stack Details" %}{% endblock %}
4
5{% block main %}
6 {% include 'project/stacks/_preview_details.html' %}
7{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_template.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_template.html
deleted file mode 100644
index e20931c..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_template.html
+++ /dev/null
@@ -1,7 +0,0 @@
1{% extends 'base.html' %}
2{% load i18n %}
3{% block title %}{% trans "Preview Template" %}{% endblock %}
4
5{% block main %}
6 {% include 'project/stacks/_preview_template.html' %}
7{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/select_template.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/select_template.html
deleted file mode 100644
index efe0b44..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/select_template.html
+++ /dev/null
@@ -1,7 +0,0 @@
1{% extends 'base.html' %}
2{% load i18n %}
3{% block title %}{% trans "Select Template" %}{% endblock %}
4
5{% block main %}
6 {% include 'project/stacks/_select_template.html' %}
7{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/update.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/update.html
deleted file mode 100644
index 61b9766..0000000
--- a/openstack_dashboard/dashboards/project/stacks/templates/stacks/update.html
+++ /dev/null
@@ -1,7 +0,0 @@
1{% extends 'base.html' %}
2{% load i18n %}
3{% block title %}{% trans "Update Stack Parameters" %}{% endblock %}
4
5{% block main %}
6 {% include 'project/stacks/_update.html' %}
7{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/tests.py b/openstack_dashboard/dashboards/project/stacks/tests.py
deleted file mode 100644
index d646a51..0000000
--- a/openstack_dashboard/dashboards/project/stacks/tests.py
+++ /dev/null
@@ -1,1003 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import json
14import re
15
16import django
17from django.conf import settings
18from django.core import exceptions
19from django.core.urlresolvers import reverse
20from django import http
21from django.test.utils import override_settings
22from django.utils import html
23from heatclient.common import template_format as hc_format
24from mox3.mox import IsA
25import six
26
27from openstack_dashboard import api
28from openstack_dashboard.dashboards.project.stacks import api as project_api
29from openstack_dashboard.dashboards.project.stacks import forms
30from openstack_dashboard.dashboards.project.stacks import mappings
31from openstack_dashboard.dashboards.project.stacks import tables
32from openstack_dashboard.test import helpers as test
33
34
35INDEX_TEMPLATE = 'horizon/common/_data_table_view.html'
36INDEX_URL = reverse('horizon:project:stacks:index')
37DETAIL_URL = 'horizon:project:stacks:detail'
38
39
40class MockResource(object):
41 def __init__(self, resource_type, physical_resource_id):
42 self.resource_type = resource_type
43 self.physical_resource_id = physical_resource_id
44
45
46class MappingsTests(test.TestCase):
47
48 def test_mappings(self):
49
50 def assertMappingUrl(url, resource_type, physical_resource_id):
51 mock = MockResource(resource_type, physical_resource_id)
52 mock_url = mappings.resource_to_url(mock)
53 self.assertEqual(url, mock_url)
54
55 assertMappingUrl(
56 '/project/networks/subnets/aaa/detail',
57 'OS::Neutron::Subnet',
58 'aaa')
59 assertMappingUrl(
60 None,
61 'OS::Neutron::Subnet',
62 None)
63 assertMappingUrl(
64 None,
65 None,
66 None)
67 assertMappingUrl(
68 None,
69 'AWS::AutoScaling::LaunchConfiguration',
70 'aaa')
71 assertMappingUrl(
72 '/project/instances/aaa/',
73 'AWS::EC2::Instance',
74 'aaa')
75 assertMappingUrl(
76 '/project/containers/container/aaa/',
77 'OS::Swift::Container',
78 'aaa')
79 assertMappingUrl(
80 None,
81 'Foo::Bar::Baz',
82 'aaa')
83 assertMappingUrl(
84 '/project/instances/aaa/',
85 'OS::Nova::Server',
86 'aaa')
87 assertMappingUrl(
88 '/project/stacks/stack/aaa/',
89 'OS::Heat::ResourceGroup',
90 'aaa')
91
92 def test_stack_output(self):
93 self.assertEqual(u'<pre>foo</pre>', mappings.stack_output('foo'))
94 self.assertEqual(u'', mappings.stack_output(None))
95
96 outputs = ['one', 'two', 'three']
97 # On Python 3, the pretty JSON output doesn't add space before newline
98 if six.PY3:
99 expected_text = """[\n "one",\n "two",\n "three"\n]"""
100 else:
101 expected_text = """[\n "one", \n "two", \n "three"\n]"""
102
103 self.assertEqual(u'<pre>%s</pre>' % html.escape(expected_text),
104 mappings.stack_output(outputs))
105
106 outputs = {'foo': 'bar'}
107 expected_text = """{\n "foo": "bar"\n}"""
108 self.assertEqual(u'<pre>%s</pre>' % html.escape(expected_text),
109 mappings.stack_output(outputs))
110
111 self.assertEqual(
112 u'<a href="http://www.example.com/foo" target="_blank">'
113 'http://www.example.com/foo</a>',
114 mappings.stack_output('http://www.example.com/foo'))
115
116
117class StackTests(test.TestCase):
118
119 @override_settings(API_RESULT_PAGE_SIZE=2)
120 @test.create_stubs({api.heat: ('stacks_list',)})
121 def test_index_paginated(self):
122 stacks = self.stacks.list()[:5]
123 filters = {}
124 api.heat.stacks_list(IsA(http.HttpRequest),
125 marker=None,
126 paginate=True,
127 sort_dir='desc',
128 filters=filters) \
129 .AndReturn([stacks, True, True])
130 api.heat.stacks_list(IsA(http.HttpRequest),
131 marker=None,
132 paginate=True,
133 sort_dir='desc',
134 filters=filters) \
135 .AndReturn([stacks[:2], True, True])
136 api.heat.stacks_list(IsA(http.HttpRequest),
137 marker=stacks[2].id,
138 paginate=True,
139 sort_dir='desc',
140 filters=filters) \
141 .AndReturn([stacks[2:4], True, True])
142 api.heat.stacks_list(IsA(http.HttpRequest),
143 marker=stacks[4].id,
144 paginate=True,
145 sort_dir='desc',
146 filters=filters) \
147 .AndReturn([stacks[4:], True, True])
148 self.mox.ReplayAll()
149
150 url = reverse('horizon:project:stacks:index')
151 res = self.client.get(url)
152 # get all
153 self.assertEqual(len(res.context['stacks_table'].data),
154 len(stacks))
155 self.assertTemplateUsed(res, INDEX_TEMPLATE)
156
157 res = self.client.get(url)
158 # get first page with 2 items
159 self.assertEqual(len(res.context['stacks_table'].data),
160 settings.API_RESULT_PAGE_SIZE)
161
162 url = "%s?%s=%s" % (reverse('horizon:project:stacks:index'),
163 tables.StacksTable._meta.pagination_param,
164 stacks[2].id)
165 res = self.client.get(url)
166 # get second page (items 2-4)
167 self.assertEqual(len(res.context['stacks_table'].data),
168 settings.API_RESULT_PAGE_SIZE)
169
170 url = "%s?%s=%s" % (reverse('horizon:project:stacks:index'),
171 tables.StacksTable._meta.pagination_param,
172 stacks[4].id)
173 res = self.client.get(url)
174 # get third page (item 5)
175 self.assertEqual(len(res.context['stacks_table'].data),
176 1)
177
178 @override_settings(API_RESULT_PAGE_SIZE=2)
179 @test.create_stubs({api.heat: ('stacks_list',)})
180 def test_index_prev_paginated(self):
181 stacks = self.stacks.list()[:3]
182 filters = {}
183 api.heat.stacks_list(IsA(http.HttpRequest),
184 marker=None,
185 paginate=True,
186 sort_dir='desc',
187 filters=filters) \
188 .AndReturn([stacks, True, False])
189 api.heat.stacks_list(IsA(http.HttpRequest),
190 marker=None,
191 paginate=True,
192 sort_dir='desc',
193 filters=filters) \
194 .AndReturn([stacks[:2], True, True])
195 api.heat.stacks_list(IsA(http.HttpRequest),
196 marker=stacks[2].id,
197 paginate=True,
198 sort_dir='desc',
199 filters=filters) \
200 .AndReturn([stacks[2:], True, True])
201 api.heat.stacks_list(IsA(http.HttpRequest),
202 marker=stacks[2].id,
203 paginate=True,
204 sort_dir='asc',
205 filters=filters) \
206 .AndReturn([stacks[:2], True, True])
207 self.mox.ReplayAll()
208
209 url = reverse('horizon:project:stacks:index')
210 res = self.client.get(url)
211 # get all
212 self.assertEqual(len(res.context['stacks_table'].data),
213 len(stacks))
214 self.assertTemplateUsed(res, INDEX_TEMPLATE)
215
216 res = self.client.get(url)
217 # get first page with 2 items
218 self.assertEqual(len(res.context['stacks_table'].data),
219 settings.API_RESULT_PAGE_SIZE)
220
221 url = "%s?%s=%s" % (reverse('horizon:project:stacks:index'),
222 tables.StacksTable._meta.pagination_param,
223 stacks[2].id)
224 res = self.client.get(url)
225 # get second page (item 3)
226 self.assertEqual(len(res.context['stacks_table'].data), 1)
227
228 url = "%s?%s=%s" % (reverse('horizon:project:stacks:index'),
229 tables.StacksTable._meta.prev_pagination_param,
230 stacks[2].id)
231 res = self.client.get(url)
232 # prev back to get first page with 2 pages
233 self.assertEqual(len(res.context['stacks_table'].data),
234 settings.API_RESULT_PAGE_SIZE)
235
236 @test.create_stubs({api.heat: ('stack_create', 'template_validate'),
237 api.neutron: ('network_list_for_tenant', )})
238 def test_launch_stack(self):
239 template = self.stack_templates.first()
240 stack = self.stacks.first()
241
242 api.heat.template_validate(IsA(http.HttpRequest),
243 files={},
244 template=hc_format.parse(template.data)) \
245 .AndReturn(json.loads(template.validate))
246
247 api.heat.stack_create(IsA(http.HttpRequest),
248 stack_name=stack.stack_name,
249 timeout_mins=60,
250 disable_rollback=True,
251 template=None,
252 parameters=IsA(dict),
253 password='password',
254 files=None)
255 api.neutron.network_list_for_tenant(IsA(http.HttpRequest),
256 self.tenant.id) \
257 .AndReturn(self.networks.list())
258 api.neutron.network_list_for_tenant(IsA(http.HttpRequest),
259 self.tenant.id) \
260 .AndReturn(self.networks.list())
261
262 self.mox.ReplayAll()
263
264 url = reverse('horizon:project:stacks:select_template')
265 res = self.client.get(url)
266 self.assertTemplateUsed(res, 'project/stacks/select_template.html')
267
268 form_data = {'template_source': 'raw',
269 'template_data': template.data,
270 'method': forms.TemplateForm.__name__}
271 res = self.client.post(url, form_data)
272 self.assertTemplateUsed(res, 'project/stacks/create.html')
273
274 url = reverse('horizon:project:stacks:launch')
275 form_data = {'template_source': 'raw',
276 'template_data': template.data,
277 'password': 'password',
278 'parameters': template.validate,
279 'stack_name': stack.stack_name,
280 "timeout_mins": 60,
281 "disable_rollback": True,
282 "__param_DBUsername": "admin",
283 "__param_LinuxDistribution": "F17",
284 "__param_InstanceType": "m1.small",
285 "__param_KeyName": "test",
286 "__param_DBPassword": "admin",
287 "__param_DBRootPassword": "admin",
288 "__param_DBName": "wordpress",
289 "__param_Network": self.networks.list()[0]['id'],
290 'method': forms.CreateStackForm.__name__}
291 res = self.client.post(url, form_data)
292 self.assertRedirectsNoFollow(res, INDEX_URL)
293
294 @test.create_stubs({api.heat: ('stack_create', 'template_validate'),
295 api.neutron: ('network_list_for_tenant', )})
296 def test_launch_stack_with_environment(self):
297 template = self.stack_templates.first()
298 environment = self.stack_environments.first()
299 stack = self.stacks.first()
300
301 api.heat.template_validate(IsA(http.HttpRequest),
302 files={},
303 template=hc_format.parse(template.data),
304 environment=environment.data) \
305 .AndReturn(json.loads(template.validate))
306
307 api.heat.stack_create(IsA(http.HttpRequest),
308 stack_name=stack.stack_name,
309 timeout_mins=60,
310 disable_rollback=True,
311 template=None,
312 environment=environment.data,
313 parameters=IsA(dict),
314 password='password',
315 files=None)
316 api.neutron.network_list_for_tenant(IsA(http.HttpRequest),
317 self.tenant.id) \
318 .AndReturn(self.networks.list())
319 api.neutron.network_list_for_tenant(IsA(http.HttpRequest),
320 self.tenant.id) \
321 .AndReturn(self.networks.list())
322
323 self.mox.ReplayAll()
324
325 url = reverse('horizon:project:stacks:select_template')
326 res = self.client.get(url)
327 self.assertTemplateUsed(res, 'project/stacks/select_template.html')
328
329 form_data = {'template_source': 'raw',
330 'template_data': template.data,
331 'environment_source': 'raw',
332 'environment_data': environment.data,
333 'method': forms.TemplateForm.__name__}
334 res = self.client.post(url, form_data)
335 self.assertTemplateUsed(res, 'project/stacks/create.html')
336
337 url = reverse('horizon:project:stacks:launch')
338 form_data = {'template_source': 'raw',
339 'template_data': template.data,
340 'environment_source': 'raw',
341 'environment_data': environment.data,
342 'password': 'password',
343 'parameters': template.validate,
344 'stack_name': stack.stack_name,
345 "timeout_mins": 60,
346 "disable_rollback": True,
347 "__param_DBUsername": "admin",
348 "__param_LinuxDistribution": "F17",
349 "__param_InstanceType": "m1.small",
350 "__param_KeyName": "test",
351 "__param_DBPassword": "admin",
352 "__param_DBRootPassword": "admin",
353 "__param_DBName": "wordpress",
354 "__param_Network": self.networks.list()[0]['id'],
355 'method': forms.CreateStackForm.__name__}
356 res = self.client.post(url, form_data)
357 self.assertRedirectsNoFollow(res, INDEX_URL)
358
359 @test.create_stubs({api.heat: ('template_validate',)})
360 def test_launch_stack_with_hidden_parameters(self):
361 template = {
362 'data': ('heat_template_version: 2013-05-23\n'
363 'parameters:\n'
364 ' public_string:\n'
365 ' type: string\n'
366 ' secret_string:\n'
367 ' type: string\n'
368 ' hidden: true\n'),
369 'validate': {
370 'Description': 'No description',
371 'Parameters': {
372 'public_string': {
373 'Label': 'public_string',
374 'Description': '',
375 'Type': 'String',
376 'NoEcho': 'false'
377 },
378 'secret_string': {
379 'Label': 'secret_string',
380 'Description': '',
381 'Type': 'String',
382 'NoEcho': 'true'
383 }
384 }
385 }
386 }
387 api.heat.template_validate(IsA(http.HttpRequest),
388 files={},
389 template=hc_format.parse(template['data'])) \
390 .AndReturn(template['validate'])
391
392 self.mox.ReplayAll()
393
394 url = reverse('horizon:project:stacks:select_template')
395 res = self.client.get(url)
396 self.assertTemplateUsed(res, 'project/stacks/select_template.html')
397
398 form_data = {'template_source': 'raw',
399 'template_data': template['data'],
400 'method': forms.TemplateForm.__name__}
401 res = self.client.post(url, form_data)
402 self.assertTemplateUsed(res, 'project/stacks/create.html')
403
404 # ensure the fields were rendered correctly
405 if django.VERSION >= (1, 10):
406 pattern = ('<input class="form-control" '
407 'id="id___param_public_string" '
408 'name="__param_public_string" type="text" required/>')
409 secret = ('<input class="form-control" '
410 'id="id___param_secret_string" '
411 'name="__param_secret_string" '
412 'type="password" required>')
413 else:
414 pattern = ('<input class="form-control" '
415 'id="id___param_public_string" '
416 'name="__param_public_string" type="text" />')
417 secret = ('<input class="form-control" '
418 'id="id___param_secret_string" '
419 'name="__param_secret_string" '
420 'type="password" />')
421
422 self.assertContains(res, pattern, html=True)
423 self.assertContains(res, secret, html=True)
424
425 @test.create_stubs({api.heat: ('template_validate',)})
426 def test_launch_stack_with_parameter_group(self):
427 template = {
428 'data': ('heat_template_version: 2013-05-23\n'
429 'parameters:\n'
430 ' last_param:\n'
431 ' type: string\n'
432 ' first_param:\n'
433 ' type: string\n'
434 ' middle_param:\n'
435 ' type: string\n'
436 'parameter_groups:\n'
437 '- parameters:\n'
438 ' - first_param\n'
439 ' - middle_param\n'
440 ' - last_param\n'),
441 'validate': {
442 'Description': 'No description',
443 'Parameters': {
444 'last_param': {
445 'Label': 'last_param',
446 'Description': '',
447 'Type': 'String',
448 'NoEcho': 'false'
449 },
450 'first_param': {
451 'Label': 'first_param',
452 'Description': '',
453 'Type': 'String',
454 'NoEcho': 'false'
455 },
456 'middle_param': {
457 'Label': 'middle_param',
458 'Description': '',
459 'Type': 'String',
460 'NoEcho': 'true'
461 }
462 },
463 'ParameterGroups': [
464 {
465 'parameters': [
466 'first_param',
467 'middle_param',
468 'last_param'
469 ]
470 }
471 ]
472 }
473 }
474 api.heat.template_validate(IsA(http.HttpRequest),
475 files={},
476 template=hc_format.parse(template['data'])) \
477 .AndReturn(template['validate'])
478
479 self.mox.ReplayAll()
480
481 url = reverse('horizon:project:stacks:select_template')
482 res = self.client.get(url)
483 self.assertTemplateUsed(res, 'project/stacks/select_template.html')
484
485 form_data = {'template_source': 'raw',
486 'template_data': template['data'],
487 'method': forms.TemplateForm.__name__}
488 res = self.client.post(url, form_data)
489 self.assertTemplateUsed(res, 'project/stacks/create.html')
490
491 # ensure the fields were rendered in the correct order
492 regex = re.compile('^.*>first_param<.*>middle_param<.*>last_param<.*$',
493 flags=re.DOTALL)
494 self.assertRegexpMatches(res.content.decode('utf-8'), regex)
495
496 @test.create_stubs({api.heat: ('stack_create', 'template_validate')})
497 def test_launch_stack_parameter_types(self):
498 template = {
499 'data': ('heat_template_version: 2013-05-23\n'
500 'parameters:\n'
501 ' param1:\n'
502 ' type: string\n'
503 ' param2:\n'
504 ' type: number\n'
505 ' param3:\n'
506 ' type: json\n'
507 ' param4:\n'
508 ' type: comma_delimited_list\n'
509 ' param5:\n'
510 ' type: boolean\n'),
511 'validate': {
512 "Description": "No description",
513 "Parameters": {
514 "param1": {
515 "Type": "String",
516 "NoEcho": "false",
517 "Description": "",
518 "Label": "param1"
519 },
520 "param2": {
521 "Type": "Number",
522 "NoEcho": "false",
523 "Description": "",
524 "Label": "param2"
525 },
526 "param3": {
527 "Type": "Json",
528 "NoEcho": "false",
529 "Description": "",
530 "Label": "param3"
531 },
532 "param4": {
533 "Type": "CommaDelimitedList",
534 "NoEcho": "false",
535 "Description": "",
536 "Label": "param4"
537 },
538 "param5": {
539 "Type": "Boolean",
540 "NoEcho": "false",
541 "Description": "",
542 "Label": "param5"
543 }
544 }
545 }
546 }
547 stack = self.stacks.first()
548
549 api.heat.template_validate(IsA(http.HttpRequest),
550 files={},
551 template=hc_format.parse(template['data'])) \
552 .AndReturn(template['validate'])
553
554 api.heat.stack_create(IsA(http.HttpRequest),
555 stack_name=stack.stack_name,
556 timeout_mins=60,
557 disable_rollback=True,
558 template=hc_format.parse(template['data']),
559 parameters={'param1': 'some string',
560 'param2': 42,
561 'param3': '{"key": "value"}',
562 'param4': 'a,b,c',
563 'param5': True},
564 password='password',
565 files={})
566
567 self.mox.ReplayAll()
568
569 url = reverse('horizon:project:stacks:select_template')
570 res = self.client.get(url)
571 self.assertTemplateUsed(res, 'project/stacks/select_template.html')
572
573 form_data = {'template_source': 'raw',
574 'template_data': template['data'],
575 'method': forms.TemplateForm.__name__}
576 res = self.client.post(url, form_data)
577 self.assertTemplateUsed(res, 'project/stacks/create.html')
578
579 # ensure the fields were rendered correctly
580 if django.VERSION >= (1, 10):
581 input_str = ('<input class="form-control" '
582 'id="id___param_param{0}" '
583 'name="__param_param{0}" type="{1}" required/>')
584 else:
585 input_str = ('<input class="form-control" '
586 'id="id___param_param{0}" '
587 'name="__param_param{0}" type="{1}"/>')
588
589 self.assertContains(res, input_str.format(1, 'text'), html=True)
590 # the custom number spinner produces an input element
591 # that doesn't match the input_strs above
592 # validate with id alone
593 self.assertContains(res, 'id="id___param_param2"')
594 self.assertContains(res, input_str.format(3, 'text'), html=True)
595 self.assertContains(res, input_str.format(4, 'text'), html=True)
596 self.assertContains(
597 res,
598 '<input id="id___param_param5" name="__param_param5" '
599 'type="checkbox">',
600 html=True)
601
602 # post some sample data and make sure it validates
603 url = reverse('horizon:project:stacks:launch')
604 form_data = {'template_source': 'raw',
605 'template_data': template['data'],
606 'password': 'password',
607 'parameters': json.dumps(template['validate']),
608 'stack_name': stack.stack_name,
609 "timeout_mins": 60,
610 "disable_rollback": True,
611 "__param_param1": "some string",
612 "__param_param2": 42,
613 "__param_param3": '{"key": "value"}',
614 "__param_param4": "a,b,c",
615 "__param_param5": True,
616 'method': forms.CreateStackForm.__name__}
617 res = self.client.post(url, form_data)
618 self.assertRedirectsNoFollow(res, INDEX_URL)
619
620 @test.create_stubs({api.heat: ('stack_update', 'stack_get', 'template_get',
621 'template_validate'),
622 api.neutron: ('network_list_for_tenant', )})
623 def test_edit_stack_template(self):
624 template = self.stack_templates.first()
625 stack = self.stacks.first()
626
627 # GET to template form
628 api.heat.stack_get(IsA(http.HttpRequest),
629 stack.id).AndReturn(stack)
630 # POST template form, validation
631 api.heat.template_validate(IsA(http.HttpRequest),
632 files={},
633 template=hc_format.parse(template.data)) \
634 .AndReturn(json.loads(template.validate))
635
636 # GET to edit form
637 api.heat.stack_get(IsA(http.HttpRequest),
638 stack.id).AndReturn(stack)
639 api.heat.template_get(IsA(http.HttpRequest),
640 stack.id) \
641 .AndReturn(json.loads(template.validate))
642
643 # POST to edit form
644 api.heat.stack_get(IsA(http.HttpRequest),
645 stack.id).AndReturn(stack)
646
647 fields = {
648 'stack_name': stack.stack_name,
649 'disable_rollback': True,
650 'timeout_mins': 61,
651 'password': 'password',
652 'template': None,
653 'parameters': IsA(dict),
654 'files': None
655 }
656 api.heat.stack_update(IsA(http.HttpRequest),
657 stack_id=stack.id,
658 **fields)
659 api.neutron.network_list_for_tenant(IsA(http.HttpRequest),
660 self.tenant.id) \
661 .AndReturn(self.networks.list())
662
663 self.mox.ReplayAll()
664
665 url = reverse('horizon:project:stacks:change_template',
666 args=[stack.id])
667 res = self.client.get(url)
668 self.assertTemplateUsed(res, 'project/stacks/change_template.html')
669
670 form_data = {'template_source': 'raw',
671 'template_data': template.data,
672 'method': forms.ChangeTemplateForm.__name__}
673 res = self.client.post(url, form_data)
674
675 url = reverse('horizon:project:stacks:edit_stack',
676 args=[stack.id, ])
677 form_data = {'template_source': 'raw',
678 'template_data': template.data,
679 'password': 'password',
680 'parameters': template.validate,
681 'stack_name': stack.stack_name,
682 'stack_id': stack.id,
683 "timeout_mins": 61,
684 "disable_rollback": True,
685 "__param_DBUsername": "admin",
686 "__param_LinuxDistribution": "F17",
687 "__param_InstanceType": "m1.small",
688 "__param_KeyName": "test",
689 "__param_DBPassword": "admin",
690 "__param_DBRootPassword": "admin",
691 "__param_DBName": "wordpress",
692 "__param_Network": self.networks.list()[0]['id'],
693 'method': forms.EditStackForm.__name__}
694 res = self.client.post(url, form_data)
695 self.assertRedirectsNoFollow(res, INDEX_URL)
696
697 def test_launch_stack_form_invalid_name_digit(self):
698 self._test_launch_stack_invalid_name('2_StartWithDigit')
699
700 def test_launch_stack_form_invalid_name_underscore(self):
701 self._test_launch_stack_invalid_name('_StartWithUnderscore')
702
703 def test_launch_stack_form_invalid_name_point(self):
704 self._test_launch_stack_invalid_name('.StartWithPoint')
705
706 @test.create_stubs({api.neutron: ('network_list_for_tenant', )})
707 def _test_launch_stack_invalid_name(self, name):
708 api.neutron.network_list_for_tenant(IsA(http.HttpRequest),
709 self.tenant.id) \
710 .AndReturn(self.networks.list())
711 self.mox.ReplayAll()
712
713 template = self.stack_templates.first()
714 url = reverse('horizon:project:stacks:launch')
715 form_data = {'template_source': 'raw',
716 'template_data': template.data,
717 'password': 'password',
718 'parameters': template.validate,
719 'stack_name': name,
720 "timeout_mins": 60,
721 "disable_rollback": True,
722 "__param_DBUsername": "admin",
723 "__param_LinuxDistribution": "F17",
724 "__param_InstanceType": "m1.small",
725 "__param_KeyName": "test",
726 "__param_DBPassword": "admin",
727 "__param_DBRootPassword": "admin",
728 "__param_DBName": "wordpress",
729 "__param_Network": self.networks.list()[0]['id'],
730 'method': forms.CreateStackForm.__name__}
731
732 res = self.client.post(url, form_data)
733 error = ('Name must start with a letter and may only contain letters, '
734 'numbers, underscores, periods and hyphens.')
735
736 self.assertFormErrors(res, 1)
737 self.assertFormError(res, "form", 'stack_name', error)
738
739 def _test_stack_action(self, action):
740 stack = self.stacks.first()
741 filters = {}
742 api.heat.stacks_list(IsA(http.HttpRequest),
743 marker=None,
744 paginate=True,
745 sort_dir='desc',
746 filters=filters) \
747 .AndReturn([self.stacks.list(), True, True])
748
749 getattr(api.heat, 'action_%s' % action)(IsA(http.HttpRequest),
750 stack.id).AndReturn(stack)
751
752 self.mox.ReplayAll()
753
754 form_data = {"action": "stacks__%s__%s" % (action, stack.id)}
755 res = self.client.post(INDEX_URL, form_data)
756
757 self.assertNoFormErrors(res)
758 self.assertRedirectsNoFollow(res, INDEX_URL)
759
760 @test.create_stubs({api.heat: ('stacks_list', 'action_check',)})
761 def test_check_stack(self):
762 self._test_stack_action('check')
763
764 @test.create_stubs({api.heat: ('stacks_list', 'action_suspend',)})
765 def test_suspend_stack(self):
766 self._test_stack_action('suspend')
767
768 @test.create_stubs({api.heat: ('stacks_list', 'action_resume',)})
769 def test_resume_stack(self):
770 self._test_stack_action('resume')
771
772 @test.create_stubs({api.heat: ('stack_preview', 'template_validate')})
773 def test_preview_stack(self):
774 template = self.stack_templates.first()
775 stack = self.stacks.first()
776
777 api.heat.template_validate(IsA(http.HttpRequest),
778 files={},
779 template=hc_format.parse(template.data)) \
780 .AndReturn(json.loads(template.validate))</