Base Glance Metadata Definitions Admin UI
Provide a base admin UI for viewing, importing, and associating the metadata definitions that can be used with various resource types such as flavors, images, and host aggregates. In Juno, Glance provided a metadata definitions catalog[1][2] where users can register the available metadata definitions that can be used on different types of resources (images, artifacts, volumes, flavors, aggregates, etc). This includes key / value pairs such as properties, extra specs, etc. Horizon landed several patches that read these properties. You can view the functionality in the "update metadata" action on Flavors, Images, and Host Aggregates. This specific patch is to bring in the Admin UI for the basic coarse grained actions on the definitions in the catalog. This includes creating (importing) a namespace, viewing the overview details about it, deleting the namespace, and associating the namespace for use with specific resource types. Future blueprints will be registered for: - CRUD on individual metadata definitions within the namespace For example, editing the default value of an individual property. [1] Approved Glance Juno Spec: https://github.com/openstack/glance-specs/blob/master/specs/juno/metadata-schema-catalog.rst [2] Glance PTL Juno Feature Overview: https://www.youtube.com/watch?v=3ptriiw1wK8&t=14m27s Co-Authored-By: Travis Tripp <travis.tripp@hp.com> Co-Authored-By: Santiago Baldassin<santiago.b.baldassin@intel.com> Co-Authored-By: Bartosz Fic <bartosz.fic@intel.com> Co-Authored-By: Pawel Koniszewski <pawel.koniszewski@intel.com> Co-Authored-By: Michal Dulko <michal.dulko@intel.com> DocImpact: Concept awareness Change-Id: Ie34007f73af7e0941631a52f03841068e509a72c Implements: blueprint glance-metadata-definitions-base-admin-ui
This commit is contained in:
parent
eb1d787cda
commit
7e5f4d1594
12
horizon/static/horizon/js/angular/controllers/namespace-controller.js
vendored
Normal file
12
horizon/static/horizon/js/angular/controllers/namespace-controller.js
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
angular.module('hz')
|
||||
.controller('hzNamespaceResourceTypeFormController', function($scope, $window) {
|
||||
$scope.resource_types = $window.resource_types;
|
||||
|
||||
$scope.saveResourceTypes = function () {
|
||||
$scope.resource_types = JSON.stringify($scope.resource_types);
|
||||
$scope.namespace = namespace;
|
||||
};
|
||||
});
|
||||
}());
|
@ -14,7 +14,7 @@
|
||||
<script src='{{ STATIC_URL }}{{ file }}' type='text/javascript' charset='utf-8'></script>
|
||||
{% endfor %}
|
||||
<script src='{{ STATIC_URL }}horizon/lib/angular/angular-cookies.js' type='text/javascript' charset='utf-8'></script>
|
||||
|
||||
<script src='{{ STATIC_URL }}horizon/js/angular/controllers/namespace-controller.js'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/angular/controllers/dummy.js' type='text/javascript' charset='utf-8'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/angular/directives/forms.js' type='text/javascript' charset='utf-8'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/angular/horizon.conf.js' type='text/javascript' charset='utf-8'></script>
|
||||
|
@ -34,6 +34,7 @@ from openstack_dashboard.api import base
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
VERSIONS = base.APIVersionManager("image", preferred_version=2)
|
||||
|
||||
|
||||
@memoized
|
||||
@ -158,10 +159,11 @@ class Namespace(BaseGlanceMetadefAPIResourceWrapper):
|
||||
'created_at', 'updated_at', 'properties', 'objects']
|
||||
|
||||
@property
|
||||
def resource_type_associations(self):
|
||||
def resource_type_names(self):
|
||||
result = [resource_type['name'] for resource_type in
|
||||
getattr(self._apiresource, 'resource_type_associations')]
|
||||
return result
|
||||
|
||||
return sorted(result)
|
||||
|
||||
@property
|
||||
def public(self):
|
||||
@ -185,10 +187,30 @@ def metadefs_namespace_get(request, namespace, resource_type=None, wrap=False):
|
||||
|
||||
def metadefs_namespace_list(request,
|
||||
filters={},
|
||||
sort_dir='desc',
|
||||
sort_key='created_at',
|
||||
sort_dir='asc',
|
||||
sort_key='namespace',
|
||||
marker=None,
|
||||
paginate=False):
|
||||
"""Retrieve a listing of Namespaces
|
||||
:param paginate: If true will perform pagination based on settings.
|
||||
:param marker: Specifies the namespace of the last-seen namespace.
|
||||
The typical pattern of limit and marker is to make an
|
||||
initial limited request and then to use the last
|
||||
namespace from the response as the marker parameter
|
||||
in a subsequent limited request. With paginate, limit
|
||||
is automatically set.
|
||||
:param sort_dir: The sort direction ('asc' or 'desc').
|
||||
:param sort_key: The field to sort on (for example, 'created_at'). Default
|
||||
is namespace. The way base namespaces are loaded into glance
|
||||
typically at first deployment is done in a single transaction
|
||||
giving them a potentially unpredictable sort result when using
|
||||
create_at.
|
||||
:param filters: specifies addition fields to filter on such as name.
|
||||
:returns A tuple of three values:
|
||||
1) Current page results
|
||||
2) A boolean of whether or not there are previous page(s).
|
||||
3) A boolean of whether or not there are more page(s).
|
||||
"""
|
||||
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
|
||||
page_size = utils.get_page_size(request)
|
||||
|
||||
@ -218,7 +240,7 @@ def metadefs_namespace_list(request,
|
||||
if marker is not None:
|
||||
has_prev_data = True
|
||||
# first page condition when reached via prev back
|
||||
elif sort_dir == 'asc' and marker is not None:
|
||||
elif sort_dir == 'desc' and marker is not None:
|
||||
has_more_data = True
|
||||
# last page condition
|
||||
elif marker is not None:
|
||||
@ -228,3 +250,43 @@ def metadefs_namespace_list(request,
|
||||
|
||||
namespaces = [Namespace(namespace) for namespace in namespaces]
|
||||
return namespaces, has_more_data, has_prev_data
|
||||
|
||||
|
||||
def metadefs_namespace_create(request, namespace):
|
||||
return glanceclient(request, '2').metadefs_namespace.create(**namespace)
|
||||
|
||||
|
||||
def metadefs_namespace_update(request, namespace_name, **properties):
|
||||
return glanceclient(request, '2').metadefs_namespace.update(
|
||||
namespace_name,
|
||||
**properties)
|
||||
|
||||
|
||||
def metadefs_namespace_delete(request, namespace_name):
|
||||
return glanceclient(request, '2').metadefs_namespace.delete(namespace_name)
|
||||
|
||||
|
||||
def metadefs_resource_types_list(request):
|
||||
return glanceclient(request, '2').metadefs_resource_type.list()
|
||||
|
||||
|
||||
def metadefs_namespace_resource_types(request, namespace_name):
|
||||
resource_types = glanceclient(request, '2').metadefs_resource_type.get(
|
||||
namespace_name)
|
||||
|
||||
# metadefs_resource_type.get() returns generator, converting it to list
|
||||
return list(resource_types)
|
||||
|
||||
|
||||
def metadefs_namespace_add_resource_type(request,
|
||||
namespace_name,
|
||||
resource_type):
|
||||
return glanceclient(request, '2').metadefs_resource_type.associate(
|
||||
namespace_name, **resource_type)
|
||||
|
||||
|
||||
def metadefs_namespace_remove_resource_type(request,
|
||||
namespace_name,
|
||||
resource_type_name):
|
||||
glanceclient(request, '2').metadefs_resource_type.deassociate(
|
||||
namespace_name, resource_type_name)
|
||||
|
@ -29,5 +29,24 @@
|
||||
"get_task": "",
|
||||
"get_tasks": "",
|
||||
"add_task": "",
|
||||
"modify_task": ""
|
||||
"modify_task": "",
|
||||
|
||||
"get_metadef_namespace": "",
|
||||
"get_metadef_namespaces":"",
|
||||
"modify_metadef_namespace":"",
|
||||
"add_metadef_namespace":"",
|
||||
"delete_metadef_namespace":"",
|
||||
|
||||
"get_metadef_object":"",
|
||||
"get_metadef_objects":"",
|
||||
"modify_metadef_object":"",
|
||||
"add_metadef_object":"",
|
||||
|
||||
"list_metadef_resource_types":"",
|
||||
"add_metadef_resource_type_association":"",
|
||||
|
||||
"get_metadef_property":"",
|
||||
"get_metadef_properties":"",
|
||||
"modify_metadef_property":"",
|
||||
"add_metadef_property":""
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ class SystemPanels(horizon.PanelGroup):
|
||||
name = _("System")
|
||||
panels = ('overview', 'metering', 'hypervisors', 'aggregates',
|
||||
'instances', 'volumes', 'flavors', 'images',
|
||||
'networks', 'routers', 'defaults', 'info')
|
||||
'networks', 'routers', 'defaults', 'metadata_defs', 'info')
|
||||
|
||||
|
||||
class Admin(horizon.Dashboard):
|
||||
|
@ -0,0 +1,26 @@
|
||||
# (c) Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
METADATA_CREATE_TEMPLATE = 'admin/metadata_defs/create.html'
|
||||
METADATA_CREATE_URL = "horizon:admin:metadata_defs:create"
|
||||
METADATA_DETAIL_OVERVIEW_TEMPLATE = "admin/metadata_defs/_detail_overview.html"
|
||||
METADATA_DETAIL_CONTENTS_TEMPLATE = "admin/metadata_defs/_detail_contents.html"
|
||||
METADATA_DETAIL_TEMPLATE = 'admin/metadata_defs/detail.html'
|
||||
METADATA_DETAIL_URL = "horizon:admin:metadata_defs:detail"
|
||||
METADATA_INDEX_TEMPLATE = 'admin/metadata_defs/index.html'
|
||||
METADATA_INDEX_URL = 'horizon:admin:metadata_defs:index'
|
||||
METADATA_MANAGE_RESOURCES_TEMPLATE = 'admin/metadata_defs/resource_types.html'
|
||||
METADATA_MANAGE_RESOURCES_URL = 'horizon:admin:metadata_defs:resource_types'
|
||||
|
||||
METADEFS_PROTECTED_PROPS = ['created_at', 'updated_at', 'owner', 'schema']
|
151
openstack_dashboard/dashboards/admin/metadata_defs/forms.py
Normal file
151
openstack_dashboard/dashboards/admin/metadata_defs/forms.py
Normal file
@ -0,0 +1,151 @@
|
||||
#
|
||||
# (c) Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Forms for managing metadata.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.forms import ValidationError # noqa
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
|
||||
from openstack_dashboard.api import glance
|
||||
from openstack_dashboard.dashboards.admin.metadata_defs \
|
||||
import constants
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CreateNamespaceForm(forms.SelfHandlingForm):
|
||||
source_type = forms.ChoiceField(
|
||||
label=_('Namespace Definition Source'),
|
||||
required=False,
|
||||
choices=[('file', _('Metadata Definition File')),
|
||||
('raw', _('Direct Input'))],
|
||||
widget=forms.Select(
|
||||
attrs={'class': 'switchable', 'data-slug': 'source'}))
|
||||
|
||||
metadef_file = forms.FileField(
|
||||
label=_("Metadata Definition File"),
|
||||
help_text=_("A local metadata definition file to upload."),
|
||||
widget=forms.FileInput(
|
||||
attrs={'class': 'switched', 'data-switch-on': 'source',
|
||||
'data-source-file': _('Metadata Definition File')}),
|
||||
required=False)
|
||||
|
||||
direct_input = forms.CharField(
|
||||
label=_('Namespace JSON'),
|
||||
help_text=_('The JSON formatted contents of a namespace.'),
|
||||
widget=forms.widgets.Textarea(
|
||||
attrs={'class': 'switched', 'data-switch-on': 'source',
|
||||
'data-source-raw': _('Namespace JSON')}),
|
||||
required=False)
|
||||
|
||||
public = forms.BooleanField(label=_("Public"), required=False)
|
||||
protected = forms.BooleanField(label=_("Protected"), required=False)
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(CreateNamespaceForm, self).__init__(request, *args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
data = super(CreateNamespaceForm, self).clean()
|
||||
|
||||
# The key can be missing based on particular upload
|
||||
# conditions. Code defensively for it here...
|
||||
metadef_file = data.get('metadef_file', None)
|
||||
metadata_raw = data.get('direct_input', None)
|
||||
|
||||
if metadata_raw and metadef_file:
|
||||
raise ValidationError(
|
||||
_("Cannot specify both file and direct input."))
|
||||
if not metadata_raw and not metadef_file:
|
||||
raise ValidationError(
|
||||
_("No input was provided for the namespace content."))
|
||||
try:
|
||||
if metadef_file:
|
||||
ns_str = self.files['metadef_file'].read()
|
||||
else:
|
||||
ns_str = data['direct_input']
|
||||
namespace = json.loads(ns_str)
|
||||
|
||||
if data['public']:
|
||||
namespace['visibility'] = 'public'
|
||||
else:
|
||||
namespace['visibility'] = 'private'
|
||||
|
||||
namespace['protected'] = data['protected']
|
||||
|
||||
for protected_prop in constants.METADEFS_PROTECTED_PROPS:
|
||||
namespace.pop(protected_prop, None)
|
||||
|
||||
data['namespace'] = namespace
|
||||
except Exception as e:
|
||||
msg = _('There was a problem loading the namespace: %s.') % e
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
return data
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
namespace = glance.metadefs_namespace_create(request,
|
||||
data['namespace'])
|
||||
messages.success(request,
|
||||
_('Namespace %s has been created.') %
|
||||
namespace['namespace'])
|
||||
return namespace
|
||||
except Exception as e:
|
||||
msg = _('Unable to create new namespace. %s')
|
||||
msg %= e.message.split('Failed validating', 1)[0]
|
||||
exceptions.handle(request, message=msg)
|
||||
return False
|
||||
|
||||
|
||||
class ManageResourceTypesForm(forms.SelfHandlingForm):
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(ManageResourceTypesForm, self).__init__(request, *args, **kwargs)
|
||||
|
||||
def handle(self, request, context):
|
||||
namespace_name = self.initial['id']
|
||||
current_names = self.get_names(self.initial['resource_types'])
|
||||
try:
|
||||
updated_types = json.loads(self.data['resource_types'])
|
||||
selected_types = [updated_type for updated_type in updated_types
|
||||
if updated_type.pop('selected', False)]
|
||||
for current_name in current_names:
|
||||
glance.metadefs_namespace_remove_resource_type(
|
||||
self.request, namespace_name, current_name)
|
||||
for selected_type in selected_types:
|
||||
selected_type.pop('$$hashKey', None)
|
||||
selected_type.pop('created_at', None)
|
||||
selected_type.pop('updated_at', None)
|
||||
glance.metadefs_namespace_add_resource_type(
|
||||
self.request, namespace_name, selected_type)
|
||||
msg = _('Resource types updated for namespace %s.')
|
||||
msg %= namespace_name
|
||||
messages.success(request, msg)
|
||||
except Exception:
|
||||
msg = _('Error updating resource types for namespace %s.')
|
||||
msg %= namespace_name
|
||||
exceptions.handle(request, msg)
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_names(self, items):
|
||||
return [item['name'] for item in items]
|
31
openstack_dashboard/dashboards/admin/metadata_defs/panel.py
Normal file
31
openstack_dashboard/dashboards/admin/metadata_defs/panel.py
Normal file
@ -0,0 +1,31 @@
|
||||
#
|
||||
# (c) Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import horizon
|
||||
|
||||
from openstack_dashboard.api import glance
|
||||
from openstack_dashboard.dashboards.admin import dashboard
|
||||
|
||||
|
||||
class MetadataDefinitions(horizon.Panel):
|
||||
name = _("Metadata Definitions")
|
||||
slug = 'metadata_defs'
|
||||
permissions = ('openstack.roles.admin',)
|
||||
|
||||
|
||||
if glance.VERSIONS.active >= 2:
|
||||
dashboard.Admin.register(MetadataDefinitions)
|
172
openstack_dashboard/dashboards/admin/metadata_defs/tables.py
Normal file
172
openstack_dashboard/dashboards/admin/metadata_defs/tables.py
Normal file
@ -0,0 +1,172 @@
|
||||
# (c) Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.template import defaultfilters as filters
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ungettext_lazy
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import tables
|
||||
|
||||
from openstack_dashboard.api import glance
|
||||
from openstack_dashboard.dashboards.admin.metadata_defs \
|
||||
import constants
|
||||
|
||||
|
||||
class ImportNamespace(tables.LinkAction):
|
||||
name = "import"
|
||||
verbose_name = _("Import Namespace")
|
||||
url = constants.METADATA_CREATE_URL
|
||||
classes = ("ajax-modal",)
|
||||
icon = "plus"
|
||||
policy_rules = (("image", "add_metadef_namespace"),)
|
||||
|
||||
|
||||
class DeleteNamespace(tables.DeleteAction):
|
||||
@staticmethod
|
||||
def action_present(count):
|
||||
return ungettext_lazy(
|
||||
u"Delete Namespace",
|
||||
u"Delete Namespaces",
|
||||
count
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def action_past(count):
|
||||
return ungettext_lazy(
|
||||
u"Deleted Namespace",
|
||||
u"Deleted Namespaces",
|
||||
count
|
||||
)
|
||||
|
||||
policy_rules = (("image", "delete_metadef_namespace"),)
|
||||
|
||||
def allowed(self, request, namespace=None):
|
||||
# Protected namespaces can not be deleted.
|
||||
if namespace and namespace.protected:
|
||||
return False
|
||||
# Return True to allow table-level bulk delete action to appear.
|
||||
return True
|
||||
|
||||
def delete(self, request, obj_id):
|
||||
glance.metadefs_namespace_delete(request, obj_id)
|
||||
|
||||
|
||||
class ManageResourceTypeAssociations(tables.LinkAction):
|
||||
name = "manage_resource_types"
|
||||
verbose_name = _("Update Associations")
|
||||
url = constants.METADATA_MANAGE_RESOURCES_URL
|
||||
classes = ("ajax-modal",)
|
||||
icon = "pencil"
|
||||
policy_rules = (("image", "list_metadef_resource_types"),
|
||||
("image", "add_metadef_resource_type_association"))
|
||||
|
||||
def allowed(self, request, namespace=None):
|
||||
# Protected namespace can not be updated
|
||||
if namespace and namespace.protected:
|
||||
return False
|
||||
# Return True to allow table-level bulk delete action to appear.
|
||||
return True
|
||||
|
||||
|
||||
class AdminMetadataFilterAction(tables.FilterAction):
|
||||
pass
|
||||
|
||||
|
||||
class UpdateRow(tables.Row):
|
||||
ajax = True
|
||||
|
||||
def get_data(self, request, namespace_name):
|
||||
return glance.metadefs_namespace_get(request,
|
||||
namespace_name,
|
||||
wrap=True)
|
||||
|
||||
|
||||
class UpdateCell(tables.UpdateAction):
|
||||
policy_rules = (("image", "modify_metadef_namespace"),)
|
||||
|
||||
def update_cell(self, request, datum, namespace_name,
|
||||
cell_name, new_cell_value):
|
||||
# inline update namespace info
|
||||
try:
|
||||
namespace_obj = datum
|
||||
# updating changed value by new value
|
||||
if cell_name == 'public':
|
||||
cell_name = 'visibility'
|
||||
if new_cell_value:
|
||||
new_cell_value = 'public'
|
||||
else:
|
||||
new_cell_value = 'private'
|
||||
setattr(namespace_obj, cell_name, new_cell_value)
|
||||
properties = {cell_name: new_cell_value}
|
||||
glance.metadefs_namespace_update(
|
||||
request,
|
||||
namespace_name,
|
||||
**properties)
|
||||
except Exception:
|
||||
exceptions.handle(request, ignore=True)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class AdminNamespacesTable(tables.DataTable):
|
||||
display_name = tables.Column(
|
||||
"display_name",
|
||||
link=constants.METADATA_DETAIL_URL,
|
||||
verbose_name=_("Name"),
|
||||
form_field=forms.CharField(max_length=80))
|
||||
description = tables.Column(
|
||||
lambda obj: getattr(obj, 'description', None),
|
||||
verbose_name=_('Description'),
|
||||
form_field=forms.CharField(widget=forms.Textarea(), required=False),
|
||||
truncate=200)
|
||||
resource_type_names = tables.Column(
|
||||
"resource_type_names",
|
||||
verbose_name=_("Resource Types"),
|
||||
wrap_list=True,
|
||||
filters=(filters.unordered_list,))
|
||||
public = tables.Column(
|
||||
"public",
|
||||
verbose_name=_("Public"),
|
||||
empty_value=False,
|
||||
form_field=forms.BooleanField(required=False),
|
||||
filters=(filters.yesno, filters.capfirst),
|
||||
update_action=UpdateCell)
|
||||
protected = tables.Column(
|
||||
"protected",
|
||||
verbose_name=_("Protected"),
|
||||
empty_value=False,
|
||||
form_field=forms.BooleanField(required=False),
|
||||
filters=(filters.yesno, filters.capfirst),
|
||||
update_action=UpdateCell)
|
||||
|
||||
def get_object_id(self, datum):
|
||||
return datum.namespace
|
||||
|
||||
def get_object_display(self, datum):
|
||||
if hasattr(datum, 'display_name'):
|
||||
return datum.display_name
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
name = "namespaces"
|
||||
verbose_name = _("Namespaces")
|
||||
row_class = UpdateRow
|
||||
table_actions = (AdminMetadataFilterAction,
|
||||
ImportNamespace,
|
||||
DeleteNamespace,)
|
||||
row_actions = (ManageResourceTypeAssociations,
|
||||
DeleteNamespace,)
|
||||
pagination_param = "namespace_marker"
|
68
openstack_dashboard/dashboards/admin/metadata_defs/tabs.py
Normal file
68
openstack_dashboard/dashboards/admin/metadata_defs/tabs.py
Normal file
@ -0,0 +1,68 @@
|
||||
# (c) Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import tabs
|
||||
|
||||
from openstack_dashboard.api import glance
|
||||
from openstack_dashboard.dashboards.admin.metadata_defs \
|
||||
import constants
|
||||
|
||||
|
||||
class OverviewTab(tabs.Tab):
|
||||
name = _("Namespace Overview")
|
||||
slug = "overview"
|
||||
template_name = constants.METADATA_DETAIL_OVERVIEW_TEMPLATE
|
||||
|
||||
def get_context_data(self, request):
|
||||
namespace_name = self.tab_group.kwargs['namespace_id']
|
||||
try:
|
||||
namespace = glance.metadefs_namespace_get(request,
|
||||
namespace_name,
|
||||
wrap=True)
|
||||
except Exception:
|
||||
namespace = None
|
||||
msg = _('Unable to retrieve namespace details.')
|
||||
exceptions.handle(request, msg)
|
||||
|
||||
return {"namespace": namespace}
|
||||
|
||||
|
||||
class ContentsTab(tabs.Tab):
|
||||
name = _("Contents")
|
||||
slug = "contents"
|
||||
template_name = constants.METADATA_DETAIL_CONTENTS_TEMPLATE
|
||||
preload = False
|
||||
|
||||
def get_context_data(self, request):
|
||||
namespace_id = self.tab_group.kwargs['namespace_id']
|
||||
try:
|
||||
namespace = glance.metadefs_namespace_get(request,
|
||||
namespace_id,
|
||||
wrap=True)
|
||||
except Exception:
|
||||
msg = _('Unable to retrieve namespace contents.')
|
||||
exceptions.handle(request, msg)
|
||||
return None
|
||||
|
||||
return {
|
||||
'namespace_name': namespace.namespace,
|
||||
"namespace_contents": namespace.as_json()}
|
||||
|
||||
|
||||
class NamespaceDetailTabs(tabs.TabGroup):
|
||||
slug = "namespace_details"
|
||||
tabs = (OverviewTab, ContentsTab)
|
@ -0,0 +1,37 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}create_metadata_form{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:admin:metadata_defs:create' %}{% endblock %}
|
||||
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
|
||||
|
||||
{% block modal-header %}{% trans "Import Namespace" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>{% trans "Description:" %}</h3>
|
||||
<p>
|
||||
{% trans "Specify a metadata definition namespace to import." %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "Only definitions in raw JSON format are supported." %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "Administrator Note: Use the following CLI command to import the default definitions into Glance: " %}
|
||||
</p>
|
||||
<p>
|
||||
glance-manage db_load_metadefs
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Import Namespace" %}" />
|
||||
<a href="{% url 'horizon:admin:metadata_defs:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,9 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class="clearfix">
|
||||
<h3 class="pull-left">{{ namespace_name|default:_("Undefined") }}</h3>
|
||||
</div>
|
||||
|
||||
<pre class="data">
|
||||
{{ namespace_contents }}
|
||||
</pre>
|
@ -0,0 +1,52 @@
|
||||
{% load i18n sizeformat %}
|
||||
|
||||
<h3>{% trans "Namespace Overview" %}</h3>
|
||||
|
||||
<div class="info row detail">
|
||||
<div class="col-sm-12">
|
||||
<h4>{% trans "Info" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Display Name" %}</dt>
|
||||
<dd>{{ namespace.display_name|default:_("None") }}</dd>
|
||||
<dt>{% trans "Namespace" %}</dt>
|
||||
<dd>{{ namespace.namespace|default:_("None") }}</dd>
|
||||
{% if namespace.description %}
|
||||
<dt>{% trans "Description" %}</dt>
|
||||
<dd>{{ namespace.description }}</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "Public" %}</dt>
|
||||
<dd>{{ namespace.public|yesno|capfirst }}</dd>
|
||||
<dt>{% trans "Protected" %}</dt>
|
||||
<dd>{{ namespace.protected|yesno|capfirst }}</dd>
|
||||
<dt>{% trans "Created" %}</dt>
|
||||
<dd>{{ namespace.created_at|parse_isotime|default:_("Unknown") }}</dd>
|
||||
<dt>{% trans "Updated" %}</dt>
|
||||
<dd>{{ namespace.updated_at|parse_isotime|default:_("Never updated") }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="resource_types row detail">
|
||||
<div class="col-sm-12">
|
||||
<h4>{% trans "Associated Resource Types" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl class="dl-horizontal">
|
||||
{% with default_item_value="<em>"|add:_("None")|add:"</em>" %}
|
||||
{% for resource_type in namespace.resource_type_associations %}
|
||||
<dt>{{ resource_type.name }}</dt>
|
||||
<dd>
|
||||
<li>{% trans "Prefix: " %}
|
||||
{{ resource_type.prefix|default:default_item_value }}
|
||||
</li>
|
||||
<li>{% trans "Properties Target: " %}
|
||||
{{ resource_type.properties_target|default:default_item_value }}
|
||||
</li>
|
||||
</dd>
|
||||
{% empty %}
|
||||
<em>{% trans "No associations defined." %}</em>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,14 @@
|
||||
{% extends 'admin/metadata_defs/resource_types.html' %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
{% block title %}
|
||||
{% trans "Namespace Resource Type Associations" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Namespace Resource Type Associations") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block form_action %}
|
||||
{% url 'horizon:admin:metadata_defs:resource_types' id %}{% endblock %}
|
||||
{% block modal-header %}
|
||||
{% trans "Namespace Resource Type Associations" %}{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Create a Metadata Namespace" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Create a Metadata Namespace") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'admin/metadata_defs/_create.html' %}
|
||||
{% endblock %}
|
@ -0,0 +1,16 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n sizeformat %}
|
||||
|
||||
{% block title %}{% trans "Namespace Details" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Namespace Details: ")|add:namespace.namespace|default:_("None") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{{ tab_group.render }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Metadata Definitions" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Metadata Definitions") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{{ table.render }}
|
||||
{% endblock %}
|
@ -0,0 +1,84 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block ng_controller %}hzNamespaceResourceTypeFormController{% endblock %}
|
||||
{% block form_name %}manageResourceTypesForm{% endblock %}
|
||||
{% block form_validation %}novalidate{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="resource-types membership">
|
||||
<div class="left">
|
||||
<div>
|
||||
<div class="header">
|
||||
<div class="fake_table fake_table_header">
|
||||
<span class="members_title">{% trans "Available Types" %}</span>
|
||||
<div class="form-group has-feedback">
|
||||
<input id="resource_type_filter"
|
||||
class="filter form-control input-sm" type="text"
|
||||
placeholder="{% trans "Filter" %}"
|
||||
name="resource_type_filter" ng-model="searchResource"
|
||||
ng-change="filter_changed()"/>
|
||||
<span class="glyphicon glyphicon-search form-control-feedback"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fake_table fake_update_members_table">
|
||||
<div class="available_members available_update_members">
|
||||
<ul ng-repeat="resource_type in resource_types | filter:searchResource"
|
||||
class="nav nav-pills btn-group {$$last ? ' last_stripe': ''$}"
|
||||
ng-class-odd="'dark_stripe'"
|
||||
ng-class-even="'light_stripe'"
|
||||
style="margin-left: 0px;">
|
||||
<li class="select_resource">
|
||||
<input type="checkbox" ng-model="resource_type.selected"/>
|
||||
</li>
|
||||
<li class="display_name">
|
||||
<span>
|
||||
{$ resource_type.name $}
|
||||
</span>
|
||||
</li>
|
||||
<li class="scope">
|
||||
<input type="text"
|
||||
class="form-control input-sm"
|
||||
placeholder="Prefix"
|
||||
ng-show="resource_type.selected"
|
||||
ng-model="resource_type.prefix"/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<h3>{% trans "Description" %}:</h3>
|
||||
|
||||
<p>{% trans "Namespaces can be associated to different resource types. This makes the properties in the namespace visible in the 'Update Metadata' action for that type of resource." %}</p>
|
||||
|
||||
<p>{% trans "Additionally, some resource types may require a prefix to be used when applying the metadata. In certain cases, the prefix may differ between the resource type (for example, flavor vs image)." %}</p>
|
||||
|
||||
<p>{% trans "Example: The prefix 'hw:' is added to OS::Nova::Flavor for the Virtual CPU Topology namespace so that the properties will be prefixed with 'hw:' when applied to flavors." %}</p>
|
||||
|
||||
<p>{% trans "Do not use a colon ':' with OS::Glance::Images. This resource type does not support the use of colons." %}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var resource_types = {{resource_types|safe}};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block modal-footer %}
|
||||
<div>
|
||||
<input class="btn btn-primary pull-right" type="submit"
|
||||
ng:click="saveResourceTypes()" value="{% trans "Save" %}"/>
|
||||
<a class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||
<input type="hidden" name="resource_types" ng-value="resource_types"
|
||||
ng-model="resource_types">
|
||||
</div>
|
||||
{% endblock %}
|
300
openstack_dashboard/dashboards/admin/metadata_defs/tests.py
Normal file
300
openstack_dashboard/dashboards/admin/metadata_defs/tests.py
Normal file
@ -0,0 +1,300 @@
|
||||
# (c) Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright 2014 Intel Corporation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import json
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django import http
|
||||
|
||||
from mox import IsA # noqa
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.admin.metadata_defs \
|
||||
import constants
|
||||
from openstack_dashboard.test import helpers as test
|
||||
|
||||
|
||||
class MetadataDefinitionsView(test.BaseAdminViewTests):
|
||||
|
||||
def test_namespace_object(self):
|
||||
mock = self.mox.CreateMockAnything()
|
||||
mock.name = 'hello'
|
||||
mock.description = 'world'
|
||||
mock.visibility = 'public'
|
||||
mock.resource_type_associations = [{'name': 'sample'}]
|
||||
|
||||
namespace = api.glance.Namespace(mock)
|
||||
self.assertEqual('world', namespace.description)
|
||||
self.assertTrue(namespace.public)
|
||||
self.assertEqual('sample', namespace.resource_type_names[0])
|
||||
|
||||
@test.create_stubs({api.glance: ('metadefs_namespace_list',)})
|
||||
def test_metadata_defs_list(self):
|
||||
namespace_list = self.metadata_defs.list()
|
||||
api.glance.metadefs_namespace_list(
|
||||
IsA(http.HttpRequest),
|
||||
sort_dir='asc',
|
||||
marker=None,
|
||||
paginate=True).AndReturn((namespace_list, False, False))
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse(constants.METADATA_INDEX_URL))
|
||||
self.assertTemplateUsed(res, constants.METADATA_INDEX_TEMPLATE)
|
||||
self.assertEqual(len(res.context['namespaces_table'].data),
|
||||
len(namespace_list))
|
||||
|
||||
@test.create_stubs({api.glance: ('metadefs_namespace_list',)})
|
||||
def test_metadata_defs_no_results(self):
|
||||
api.glance.metadefs_namespace_list(
|
||||
IsA(http.HttpRequest),
|
||||
sort_dir='asc',
|
||||
marker=None,
|
||||
paginate=True).AndReturn(((), False, False))
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse(constants.METADATA_INDEX_URL))
|
||||
self.assertTemplateUsed(res, constants.METADATA_INDEX_TEMPLATE)
|
||||
self.assertEqual(len(res.context['namespaces_table'].data), 0)
|
||||
|
||||
@test.create_stubs({api.glance: ('metadefs_namespace_list',)})
|
||||
def test_metadata_defs_error(self):
|
||||
api.glance.metadefs_namespace_list(
|
||||
IsA(http.HttpRequest),
|
||||
sort_dir='asc',
|
||||
marker=None,
|
||||
paginate=True).AndRaise(self.exceptions.glance)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse(constants.METADATA_INDEX_URL))
|
||||
self.assertTemplateUsed(res, constants.METADATA_INDEX_TEMPLATE)
|
||||
|
||||
@test.create_stubs({api.glance: ('metadefs_namespace_list',)})
|
||||
def test_delete_availability(self):
|
||||
namespace_list = self.metadata_defs.list()
|
||||
api.glance.metadefs_namespace_list(
|
||||
IsA(http.HttpRequest),
|
||||
sort_dir='asc',
|
||||
marker=None,
|
||||
paginate=True).AndReturn((namespace_list, False, False))
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse(constants.METADATA_INDEX_URL))
|
||||
self.assertIn('namespaces_table', res.context)
|
||||
ns_table = res.context['namespaces_table']
|
||||
namespaces = ns_table.data
|
||||
|
||||
for i in [1, 2]:
|
||||
row_actions = ns_table.get_row_actions(namespaces[i])
|
||||
self.assertTrue(len(row_actions), 2)
|
||||
self.assertTrue('delete' in
|
||||
[a.name for a in row_actions])
|
||||
self.assertTrue('manage_resource_types' in
|
||||
[a.name for a in row_actions])
|
||||
|
||||
@test.create_stubs({api.glance: ('metadefs_namespace_get',)})
|
||||
def test_metadata_defs_get(self):
|
||||
namespace = self.metadata_defs.first()
|
||||
api.glance.metadefs_namespace_get(
|
||||
IsA(http.HttpRequest),
|
||||
'1',
|
||||
wrap=True
|
||||
).MultipleTimes().AndReturn(namespace)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse(constants.METADATA_DETAIL_URL,
|
||||
kwargs={'namespace_id': '1'}))
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertTemplateUsed(res, constants.METADATA_DETAIL_TEMPLATE)
|
||||
|
||||
@test.create_stubs({api.glance: ('metadefs_namespace_get',)})
|
||||
def test_metadata_defs_get_contents(self):
|
||||
namespace = self.metadata_defs.first()
|
||||
api.glance.metadefs_namespace_get(
|
||||
IsA(http.HttpRequest),
|
||||
'1',
|
||||
wrap=True
|
||||
).MultipleTimes().AndReturn(namespace)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(
|
||||
'?'.join([reverse(constants.METADATA_DETAIL_URL,
|
||||
kwargs={'namespace_id': '1'}),
|
||||
'='.join(['tab', 'namespace_details__contents'])]))
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertTemplateUsed(res, constants.METADATA_DETAIL_TEMPLATE)
|
||||
|
||||
@test.create_stubs({api.glance: ('metadefs_namespace_get',)})
|
||||
def test_metadata_defs_get_overview(self):
|
||||
namespace = self.metadata_defs.first()
|
||||
api.glance.metadefs_namespace_get(
|
||||
IsA(http.HttpRequest),
|
||||
'1',
|
||||
wrap=True
|
||||
).MultipleTimes().AndReturn(namespace)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(
|
||||
'?'.join([reverse(constants.METADATA_DETAIL_URL,
|
||||
kwargs={'namespace_id': '1'}),
|
||||
'='.join(['tab', 'namespace_details__overview'])]))
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertTemplateUsed(res, constants.METADATA_DETAIL_TEMPLATE)
|
||||
|
||||
@test.create_stubs({api.glance: ('metadefs_resource_types_list',
|
||||
'metadefs_namespace_resource_types')})
|
||||
def test_metadata_defs_manage_resource_types(self):
|
||||
namespace = self.metadata_defs.first()
|
||||
api.glance.metadefs_namespace_resource_types(
|
||||
IsA(http.HttpRequest),
|
||||
'1'
|
||||
).AndReturn(namespace.resource_type_associations)
|
||||
api.glance.metadefs_resource_types_list(
|
||||
IsA(http.HttpRequest)
|
||||
).AndReturn(namespace.resource_type_associations)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(
|
||||
reverse(constants.METADATA_MANAGE_RESOURCES_URL,
|
||||
kwargs={'id': '1'}))
|
||||
|
||||
self.assertTemplateUsed(res,
|
||||
constants.METADATA_MANAGE_RESOURCES_TEMPLATE)
|
||||
self.assertContains(res, 'mock name')
|
||||
|
||||
@test.create_stubs({api.glance: ('metadefs_namespace_resource_types',
|
||||
'metadefs_namespace_remove_resource_type',
|
||||
'metadefs_namespace_add_resource_type')})
|
||||
def test_metadata_defs_manage_resource_types_change(self):
|
||||
resource_type_associations = [
|
||||
{
|
||||
'prefix': 'mock1_prefix',
|
||||
'name': 'mock1'
|
||||
},
|
||||
{
|
||||
'prefix': 'mock2_prefix',
|
||||
'name': 'mock2',
|
||||
'selected': True
|
||||
}
|
||||
]
|
||||
|
||||
api.glance.metadefs_namespace_resource_types(
|
||||
IsA(http.HttpRequest),
|
||||
'1'
|
||||
).AndReturn(resource_type_associations)
|
||||
api.glance.metadefs_namespace_remove_resource_type(
|
||||
IsA(http.HttpRequest),
|
||||
'1',
|
||||
'mock1'
|
||||
).AndReturn(resource_type_associations)
|
||||
api.glance.metadefs_namespace_remove_resource_type(
|
||||
IsA(http.HttpRequest),
|
||||
'1',
|
||||
'mock2'
|
||||
).AndReturn(resource_type_associations)
|
||||
api.glance.metadefs_namespace_add_resource_type(
|
||||
IsA(http.HttpRequest),
|
||||
'1',
|
||||
{
|
||||
'prefix': 'mock2_prefix',
|
||||
'name': 'mock2'
|
||||
}
|
||||
).AndReturn(resource_type_associations)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
form_data = {'resource_types': json.dumps(resource_type_associations)}
|
||||
res = self.client.post(
|
||||
reverse(constants.METADATA_MANAGE_RESOURCES_URL,
|
||||
kwargs={'id': '1'}),
|
||||
form_data)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertMessageCount(success=1)
|
||||
self.assertRedirectsNoFollow(
|
||||
res, reverse(constants.METADATA_INDEX_URL)
|
||||
)
|
||||
|
||||
|
||||
class MetadataDefinitionsCreateViewTest(test.BaseAdminViewTests):
|
||||
|
||||
def test_admin_metadata_defs_create_namespace_get(self):
|
||||
res = self.client.get(reverse(constants.METADATA_CREATE_URL))
|
||||
self.assertTemplateUsed(res, constants.METADATA_CREATE_TEMPLATE)
|
||||
|
||||
@test.create_stubs({api.glance: ('metadefs_namespace_create',)})
|
||||
def test_admin_metadata_defs_create_namespace_post(self):
|
||||
metadata = {}
|
||||
metadata["namespace"] = "test_namespace"
|
||||
metadata["display_name"] = "display_name"
|
||||
metadata["description"] = "description"
|
||||
metadata["visibility"] = "private"
|
||||
metadata["protected"] = False
|
||||
|
||||
api.glance.metadefs_namespace_create(
|
||||
IsA(http.HttpRequest),
|
||||
metadata
|
||||
).AndReturn(metadata)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
form_data = {
|
||||
'source_type': 'raw',
|
||||
'direct_input': json.dumps(metadata)
|
||||
}
|
||||
|
||||
res = self.client.post(reverse(constants.METADATA_CREATE_URL),
|
||||
form_data)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
|
||||
def test_admin_metadata_defs_create_namespace_invalid_json_post_raw(self):
|
||||
form_data = {
|
||||
'source_type': 'raw',
|
||||
'direct_input': 'invalidjson'
|
||||
}
|
||||
|
||||
res = self.client.post(reverse(constants.METADATA_CREATE_URL),
|
||||
form_data)
|
||||
|
||||
self.assertFormError(res, "form", None, ['There was a problem loading '
|
||||
'the namespace: No JSON '
|
||||
'object could be decoded.'])
|
||||
|
||||
def test_admin_metadata_defs_create_namespace_empty_json_post_raw(self):
|
||||
form_data = {
|
||||
'source_type': 'raw',
|
||||
'direct_input': ''
|
||||
}
|
||||
|
||||
res = self.client.post(reverse(constants.METADATA_CREATE_URL),
|
||||
form_data)
|
||||
|
||||
self.assertFormError(res, "form", None, ['No input was provided for '
|
||||
'the namespace content.'])
|
||||
|
||||
def test_admin_metadata_defs_create_namespace_empty_json_post_file(self):
|
||||
form_data = {
|
||||
'source_type': 'raw',
|
||||
'direct_input': ''
|
||||
}
|
||||
|
||||
res = self.client.post(reverse(constants.METADATA_CREATE_URL),
|
||||
form_data)
|
||||
|
||||
self.assertFormError(res, "form", None, ['No input was provided for '
|
||||
'the namespace content.'])
|
31
openstack_dashboard/dashboards/admin/metadata_defs/urls.py
Normal file
31
openstack_dashboard/dashboards/admin/metadata_defs/urls.py
Normal file
@ -0,0 +1,31 @@
|
||||
# (c) Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf.urls import patterns # noqa
|
||||
from django.conf.urls import url # noqa
|
||||
|
||||
from openstack_dashboard.dashboards.admin.metadata_defs import views
|
||||
|
||||
|
||||
NAMESPACES = r'^(?P<namespace_id>[^/]+)/%s$'
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'openstack_dashboard.dashboards.admin.metadata_defs.views',
|
||||
url(r'^$', views.AdminIndexView.as_view(), name='index'),
|
||||
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
url(NAMESPACES % 'detail', views.DetailView.as_view(), name='detail'),
|
||||
url(r'^(?P<id>[^/]+)/resource_types/$',
|
||||
views.ManageResourceTypes.as_view(), name='resource_types'),
|
||||
)
|
162
openstack_dashboard/dashboards/admin/metadata_defs/views.py
Normal file
162
openstack_dashboard/dashboards/admin/metadata_defs/views.py
Normal file
@ -0,0 +1,162 @@
|
||||
# (c) Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import tables
|
||||
from horizon import tabs
|
||||
from horizon.utils import memoized
|
||||
|
||||
from openstack_dashboard.api import glance
|
||||
from openstack_dashboard.dashboards.admin.metadata_defs \
|
||||
import constants
|
||||
from openstack_dashboard.dashboards.admin.metadata_defs \
|
||||
import forms as admin_forms
|
||||
from openstack_dashboard.dashboards.admin.metadata_defs \
|
||||
import tables as admin_tables
|
||||
from openstack_dashboard.dashboards.admin.metadata_defs \
|
||||
import tabs as admin_tabs
|
||||
|
||||
|
||||
class AdminIndexView(tables.DataTableView):
|
||||
table_class = admin_tables.AdminNamespacesTable
|
||||
template_name = constants.METADATA_INDEX_TEMPLATE
|
||||
|
||||
def has_prev_data(self, table):
|
||||
return self._prev
|
||||
|
||||
def has_more_data(self, table):
|
||||
return self._more
|
||||
|
||||
def get_data(self):
|
||||
namespaces = []
|
||||
prev_marker = self.request.GET.get(
|
||||
admin_tables.AdminNamespacesTable._meta.prev_pagination_param,
|
||||
None)
|
||||
|
||||
if prev_marker is not None:
|
||||
sort_dir = 'desc'
|
||||
marker = prev_marker
|
||||
else:
|
||||
sort_dir = 'asc'
|
||||
marker = self.request.GET.get(
|
||||
admin_tables.AdminNamespacesTable._meta.pagination_param, None)
|
||||
|
||||
try:
|
||||
namespaces, self._more, self._prev =\
|
||||
glance.metadefs_namespace_list(self.request,
|
||||
marker=marker,
|
||||
paginate=True,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
if prev_marker is not None:
|
||||
namespaces = sorted(namespaces,
|
||||
key=lambda ns: getattr(ns, 'namespace'),
|
||||
reverse=True)
|
||||
except Exception:
|
||||
self._prev = False
|
||||
self._more = False
|
||||
msg = _('Error getting metadata definitions.')
|
||||
exceptions.handle(self.request, msg)
|
||||
return namespaces
|
||||
|
||||
|
||||
class CreateView(forms.ModalFormView):
|
||||
form_class = admin_forms.CreateNamespaceForm
|
||||
template_name = constants.METADATA_CREATE_TEMPLATE
|
||||
context_object_name = 'namespace'
|
||||
success_url = reverse_lazy(constants.METADATA_INDEX_URL)
|
||||
|
||||
|
||||
class DetailView(tabs.TabView):
|
||||
redirect_url = constants.METADATA_INDEX_URL
|
||||
|
||||
tab_group_class = admin_tabs.NamespaceDetailTabs
|
||||
template_name = constants.METADATA_DETAIL_TEMPLATE
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
context["namespace"] = self.get_data()
|
||||
return context
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_data(self):
|
||||
try:
|
||||
namespace = glance.metadefs_namespace_get(
|
||||
self.request, self.kwargs['namespace_id'], wrap=True)
|
||||
except Exception:
|
||||
url = reverse_lazy(constants.METADATA_INDEX_URL)
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve namespace details.'),
|
||||
redirect=url)
|
||||
else:
|
||||
return namespace
|
||||
|
||||
def get_tabs(self, request, *args, **kwargs):
|
||||
namespace = self.get_data()
|
||||
return self.tab_group_class(request, namespace=namespace, **kwargs)
|
||||
|
||||
|
||||
class ManageResourceTypes(forms.ModalFormView):
|
||||
template_name = constants.METADATA_MANAGE_RESOURCES_TEMPLATE
|
||||
form_class = admin_forms.ManageResourceTypesForm
|
||||
success_url = reverse_lazy(constants.METADATA_INDEX_URL)
|
||||
|
||||
def get_initial(self):
|
||||
try:
|
||||
resource_types = glance.metadefs_namespace_resource_types(
|
||||
self.request, self.kwargs["id"])
|
||||
except Exception:
|
||||
resource_types = []
|
||||
msg = _('Error getting resource type associations.')
|
||||
exceptions.handle(self.request, msg)
|
||||
return {'id': self.kwargs["id"],
|
||||
'resource_types': resource_types}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ManageResourceTypes, self).get_context_data(**kwargs)
|
||||
|
||||
selected_type_names = [selected_type['name'] for selected_type in
|
||||
context['form'].initial['resource_types']]
|
||||
|
||||
try:
|
||||
# Set the basic types that aren't already associated
|
||||
result = [unselected_type for unselected_type in
|
||||
glance.metadefs_resource_types_list(self.request)
|
||||
if unselected_type['name'] not in selected_type_names]
|
||||
except Exception:
|
||||
result = []
|
||||
msg = _('Error getting resource type associations.')
|
||||
exceptions.handle(self.request, msg)
|
||||
|
||||
# Add the resource types previously associated, includes prefix, etc
|
||||
for initial_type in context['form'].initial['resource_types']:
|
||||
selected_type = initial_type.copy()
|
||||
selected_type['selected'] = True
|
||||
result.insert(0, selected_type)
|
||||
|
||||
context['id'] = self.kwargs['id']
|
||||
try:
|
||||
context["resource_types"] = json.dumps(result)
|
||||
except Exception:
|
||||
context["resource_types"] = "[]"
|
||||
msg = _('Error getting resource type associations.')
|
||||
exceptions.handle(self.request, msg)
|
||||
|
||||
return context
|
@ -1450,6 +1450,24 @@ label.log-length {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
li.scope input{
|
||||
background: none;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 120px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
li.select_resource {
|
||||
margin-left: 5px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
li.display_name {
|
||||
width: 130px;
|
||||
margin: 15px 7px 15px 10px;
|
||||
}
|
||||
|
||||
/* Member lists */
|
||||
.members, .available_members {
|
||||
padding: 0;
|
||||
@ -1532,6 +1550,28 @@ label.log-length {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.resource-types {
|
||||
.membership {
|
||||
li .scope {
|
||||
background: none;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
li .select_resource {
|
||||
margin-left: 5px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
li .display_name {
|
||||
width: 130px;
|
||||
margin-left: 10px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Fixes overflow on dropdowns in modal */
|
||||
.dropdown_fix {
|
||||
overflow: visible;
|
||||
|
Loading…
Reference in New Issue
Block a user