Enable flavors metadata update

Nova currently expose an api to let the users update
the flavors metadata so horizon should expose
this functionality. At the same time Glance is exposing
a metadata dictionary in Juno, where users can register
the key/value pairs that describe their cloud deployment.
This patch uses the glance metadata api to expose the available
metadata and the nova api to update the aggregates metadata.

Implements: blueprint glaance-metadata-flavors

Co-Authored-By: Santiago Baldassin <santiago.b.baldassin@intel.com>
Co-Authored-By: Pawel Skowron <pawel.skowron@intel.com>
Co-Authored-By: Pawel Koniszewski <pawel.koniszewski@intel.com>
Co-Authored-By: Michal Dulko <michal.dulko@intel.com>

DocImpact:
    New UI. FLAVOR_EXTRA_KEYS setting deprecated.

Change-Id: Ica9d3d06314a4d5afa77f644bd1f8900a1c328f7
This commit is contained in:
Michal Dulko 2014-09-15 16:48:06 +02:00
parent bd998fb609
commit d5fea888a8
26 changed files with 341 additions and 624 deletions

View File

@ -263,26 +263,6 @@ This example sorts flavors by vcpus in descending order::
'reverse': True,
}
``FLAVOR_EXTRA_KEYS``
---------------------
.. versionadded:: 2014.1(Icehouse)
Default::
{
'flavor_keys': [
('quota:disk_read_bytes_sec', _('Quota: Read bytes')),
('quota:disk_write_bytes_sec', _('Quota: Write bytes')),
('quota:cpu_quota', _('Quota: CPU')),
('quota:cpu_period', _('Quota: CPU period')),
('quota:vif_inbound_average', _('Quota: Inbound average')),
('quota:vif_outbound_average', _('Quota: Outbound average'))
]
}
Used to customize flavor extra specs keys
``IMAGES_LIST_FILTER_TENANTS``
------------------------------

View File

@ -0,0 +1,21 @@
# 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.
FLAVORS_TEMPLATE_NAME = 'admin/flavors/index.html'
FLAVORS_INDEX_URL = 'horizon:admin:flavors:index'
FLAVORS_CREATE_URL = 'horizon:admin:flavors:create'
FLAVORS_CREATE_VIEW_TEMPLATE = 'admin/flavors/create.html'
FLAVORS_UPDATE_URL = 'horizon:admin:flavors:update'
FLAVORS_UPDATE_VIEW_TEMPLATE = 'admin/flavors/update.html'
FLAVORS_UPDATE_METADATA_URL = 'horizon:admin:flavors:update_metadata'
FLAVORS_UPDATE_METADATA_TEMPLATE = 'admin/flavors/update_metadata.html'
FLAVORS_UPDATE_METADATA_SUBTEMPLATE = 'admin/flavors/_update_metadata.html'

View File

@ -1,98 +0,0 @@
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright (c) 2012 Intel, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import settings
from django.forms import ValidationError # noqa
from django.utils.translation import ugettext_lazy as _
from openstack_dashboard import api
from horizon import exceptions
from horizon import forms
from horizon import messages
import re
class CreateExtraSpec(forms.SelfHandlingForm):
_extraspec_name_regex = re.compile(r"^[\w\.\-: ]+$", re.UNICODE)
keys = forms.ChoiceField(label=_("Keys"),
widget=forms.Select(attrs={
'class': 'switchable',
'data-slug': 'keys'}))
key = forms.RegexField(
max_length=255,
label=_("Key"),
required=False,
regex=_extraspec_name_regex,
error_messages={'invalid': _('Key Name may only contain letters, '
'numbers, underscores, periods, colons, '
'spaces and hyphens.')},
widget=forms.TextInput(attrs={
'class': 'switched',
'data-switch-on': 'keys',
'data-keys-custom': _('Key')}))
value = forms.CharField(max_length=255, label=_("Value"))
flavor_id = forms.CharField(widget=forms.widgets.HiddenInput)
def __init__(self, *args, **kwargs):
super(CreateExtraSpec, self).__init__(*args, **kwargs)
key_settings = getattr(settings, 'FLAVOR_EXTRA_KEYS', {})
key_list = key_settings.get('flavor_keys', [])
self.fields['keys'].choices = key_list + [('custom', _('Other Key'))]
def clean(self):
cleaned_data = super(CreateExtraSpec, self).clean()
keys = cleaned_data.get('keys', None)
key = cleaned_data.get('key', None)
if keys == 'custom' and key == "":
msg = _('This field is required.')
self._errors["key"] = self.error_class([msg])
return cleaned_data
def handle(self, request, data):
if data["keys"] != 'custom':
data['key'] = data['keys']
try:
api.nova.flavor_extra_set(request,
data['flavor_id'],
{data['key']: data['value']})
msg = _('Created extra spec "%s".') % data['key']
messages.success(request, msg)
return True
except Exception:
exceptions.handle(request,
_("Unable to create flavor extra spec."))
class EditExtraSpec(forms.SelfHandlingForm):
key = forms.CharField(widget=forms.widgets.HiddenInput)
value = forms.CharField(max_length=255, label=_("Value"))
flavor_id = forms.CharField(widget=forms.widgets.HiddenInput)
def handle(self, request, data):
flavor_id = data['flavor_id']
try:
api.nova.flavor_extra_set(request,
flavor_id,
{data['key']: data['value']})
msg = _('Saved extra spec "%s".') % data['key']
messages.success(request, msg)
return True
except Exception:
exceptions.handle(request, _("Unable to edit extra spec."))

View File

@ -1,69 +0,0 @@
# Copyright (c) 2012 Intel, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import tables
from openstack_dashboard import api
class ExtraSpecDelete(tables.DeleteAction):
data_type_singular = _("ExtraSpec")
data_type_plural = _("ExtraSpecs")
def delete(self, request, obj_ids):
flavor = api.nova.flavor_get(request, self.table.kwargs['id'])
flavor.unset_keys([obj_ids])
class ExtraSpecCreate(tables.LinkAction):
name = "create"
verbose_name = _("Create")
url = "horizon:admin:flavors:extras:create"
classes = ("ajax-modal",)
icon = "plus"
def get_link_url(self, extra_spec=None):
return reverse(self.url, args=[self.table.kwargs['id']])
class ExtraSpecEdit(tables.LinkAction):
name = "edit"
verbose_name = _("Edit")
url = "horizon:admin:flavors:extras:edit"
classes = ("ajax-modal",)
icon = "pencil"
def get_link_url(self, extra_spec):
return reverse(self.url, args=[self.table.kwargs['id'],
extra_spec.key])
class ExtraSpecsTable(tables.DataTable):
key = tables.Column('key', verbose_name=_('Key'))
value = tables.Column('value', verbose_name=_('Value'))
class Meta:
name = "extras"
verbose_name = _("Extra Specs")
table_actions = (ExtraSpecCreate, ExtraSpecDelete)
row_actions = (ExtraSpecEdit, ExtraSpecDelete)
def get_object_id(self, datum):
return datum.key
def get_object_display(self, datum):
return datum.key

View File

@ -1,127 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.core.urlresolvers import reverse
from django import http
from mox import IsA # noqa
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
class FlavorExtrasTests(test.BaseAdminViewTests):
@test.create_stubs({api.nova: ('flavor_get_extras',
'flavor_get'), })
def test_list_extras_when_none_exists(self):
flavor = self.flavors.first()
extras = [api.nova.FlavorExtraSpec(flavor.id, 'k1', 'v1')]
# GET -- to determine correctness of output
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id).AndReturn(flavor)
api.nova.flavor_get_extras(IsA(http.HttpRequest),
flavor.id).AndReturn(extras)
self.mox.ReplayAll()
url = reverse('horizon:admin:flavors:extras:index', args=[flavor.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp, "admin/flavors/extras/index.html")
@test.create_stubs({api.nova: ('flavor_extra_set', ), })
def _generic_extra_create_post(self, key_name):
flavor = self.flavors.first()
create_url = reverse('horizon:admin:flavors:extras:create',
args=[flavor.id])
index_url = reverse('horizon:admin:flavors:extras:index',
args=[flavor.id])
# GET to display the flavor_name
api.nova.flavor_extra_set(IsA(http.HttpRequest),
flavor.id,
{key_name: 'v1'})
self.mox.ReplayAll()
data = {'flavor_id': flavor.id,
'keys': 'custom',
'key': key_name,
'value': 'v1'}
resp = self.client.post(create_url, data)
self.assertNoFormErrors(resp)
self.assertRedirectsNoFollow(resp, index_url)
self.mox.UnsetStubs()
@test.create_stubs({api.nova: ('flavor_extra_set', ), })
def test_extra_create_with_template(self):
flavor = self.flavors.first()
create_url = reverse('horizon:admin:flavors:extras:create',
args=[flavor.id])
index_url = reverse('horizon:admin:flavors:extras:index',
args=[flavor.id])
# GET to display the flavor_name
api.nova.flavor_extra_set(IsA(http.HttpRequest),
flavor.id,
{'quota:disk_read_bytes_sec': '1000'})
self.mox.ReplayAll()
data = {'flavor_id': flavor.id,
'keys': 'quota:disk_read_bytes_sec',
'value': '1000'}
resp = self.client.post(create_url, data)
self.assertNoFormErrors(resp)
self.assertRedirectsNoFollow(resp, index_url)
@test.create_stubs({api.nova: ('flavor_get', ), })
def test_extra_create_get(self):
flavor = self.flavors.first()
create_url = reverse('horizon:admin:flavors:extras:create',
args=[flavor.id])
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id).AndReturn(flavor)
self.mox.ReplayAll()
resp = self.client.get(create_url)
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp,
'admin/flavors/extras/create.html')
@test.create_stubs({api.nova: ('flavor_get', ), })
def _generic_extra_create_names_format_fail(self, key_name):
flavor = self.flavors.first()
create_url = reverse('horizon:admin:flavors:extras:create',
args=[flavor.id])
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id).AndReturn(flavor)
self.mox.ReplayAll()
data = {'flavor_id': flavor.id,
'keys': 'custom',
'key': key_name,
'value': 'v1'}
resp = self.client.post(create_url, data)
msg = ('Name may only contain letters, numbers, underscores, periods, '
'colons, spaces and hyphens.')
self.assertFormErrors(resp, 1, msg)
self.mox.UnsetStubs()
def test_create_extra_key_names_valid_formats(self):
valid_keys = ("key1", "month.price", "I-Am:AK-ey. 22-")
for x in valid_keys:
self._generic_extra_create_post(key_name=x)
def test_create_extra_key_names_invalid_formats(self):
invalid_keys = ("key1/", "<key>", "$$akey$", "!akey")
for x in invalid_keys:
self._generic_extra_create_names_format_fail(key_name=x)

View File

@ -1,28 +0,0 @@
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# 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.flavors.extras import views
urlpatterns = patterns('',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<key>[^/]+)/edit/$', views.EditView.as_view(), name='edit')
)

View File

@ -1,100 +0,0 @@
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright (c) 2012 Intel, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.flavors.extras \
import forms as project_forms
from openstack_dashboard.dashboards.admin.flavors.extras \
import tables as project_tables
class ExtraSpecMixin(object):
def get_context_data(self, **kwargs):
context = super(ExtraSpecMixin, self).get_context_data(**kwargs)
try:
context['flavor'] = api.nova.flavor_get(self.request,
self.kwargs['id'])
except Exception:
exceptions.handle(self.request,
_("Unable to retrieve flavor details."))
if 'key' in self.kwargs:
context['key'] = self.kwargs['key']
return context
class IndexView(ExtraSpecMixin, forms.ModalFormMixin, tables.DataTableView):
table_class = project_tables.ExtraSpecsTable
template_name = 'admin/flavors/extras/index.html'
def get_data(self):
try:
flavor_id = self.kwargs['id']
extras_list = api.nova.flavor_get_extras(self.request, flavor_id)
extras_list.sort(key=lambda es: (es.key,))
except Exception:
extras_list = []
exceptions.handle(self.request,
_('Unable to retrieve extra spec list.'))
return extras_list
class CreateView(ExtraSpecMixin, forms.ModalFormView):
form_class = project_forms.CreateExtraSpec
template_name = 'admin/flavors/extras/create.html'
def get_initial(self):
return {'flavor_id': self.kwargs['id']}
def get_success_url(self):
return reverse("horizon:admin:flavors:extras:index",
args=(self.kwargs["id"],))
class EditView(ExtraSpecMixin, forms.ModalFormView):
form_class = project_forms.EditExtraSpec
template_name = 'admin/flavors/extras/edit.html'
success_url = 'horizon:admin:flavors:extras:index'
def get_success_url(self):
return reverse(self.success_url,
args=(self.kwargs['id'],))
def get_initial(self):
flavor_id = self.kwargs['id']
key = self.kwargs['key']
try:
extra_specs = api.nova.flavor_get_extras(self.request,
flavor_id,
raw=True)
except Exception:
extra_specs = {}
exceptions.handle(self.request,
_('Unable to retrieve flavor extra spec '
'details.'))
return {'flavor_id': flavor_id,
'key': key,
'value': extra_specs.get(key, '')}

View File

@ -0,0 +1,50 @@
# 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.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from openstack_dashboard import api
class UpdateMetadataForm(forms.SelfHandlingForm):
def handle(self, request, data):
id = self.initial['id']
old_metadata = self.initial['metadata']
try:
new_metadata = json.loads(self.data['metadata'])
metadata = dict(
(item['key'], str(item['value']))
for item in new_metadata
)
api.nova.flavor_extra_set(request, id, metadata)
remove_keys = [key for key in old_metadata if key not in metadata]
api.nova.flavor_extra_delete(request, id, remove_keys)
message = _('Metadata successfully updated.')
messages.success(request, message)
except Exception:
exceptions.handle(request,
_('Unable to update the flavor metadata.'))
return False
return True

View File

@ -50,10 +50,11 @@ class UpdateFlavor(tables.LinkAction):
icon = "pencil"
class ViewFlavorExtras(tables.LinkAction):
name = "extras"
verbose_name = _("View Extra Specs")
url = "horizon:admin:flavors:extras:index"
class UpdateMetadata(tables.LinkAction):
url = "horizon:admin:flavors:update_metadata"
name = "update_metadata"
verbose_name = _("Update Metadata")
classes = ("ajax-modal",)
icon = "pencil"
@ -124,7 +125,8 @@ class FlavorsTable(tables.DataTable):
filters=(filters.yesno, filters.capfirst))
extra_specs = tables.Column(get_extra_specs,
verbose_name=_("Extra Specs"),
link=("horizon:admin:flavors:extras:index"),
link="horizon:admin:flavors:update_metadata",
link_classes=("ajax-modal",),
empty_value=False,
filters=(filters.yesno, filters.capfirst))
@ -134,5 +136,5 @@ class FlavorsTable(tables.DataTable):
table_actions = (FlavorFilterAction, CreateFlavor, DeleteFlavor)
row_actions = (UpdateFlavor,
ModifyAccess,
ViewFlavorExtras,
UpdateMetadata,
DeleteFlavor)

View File

@ -0,0 +1,11 @@
{% extends 'horizon/common/_modal_form_update_metadata.html' %}
{% load i18n %}
{% load url from future %}
{% block title %}{% trans "Update Flavor Metadata" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Update Flavor Metadata") %}
{% endblock page_header %}
{% block form_action %}{% url 'horizon:admin:flavors:update_metadata' id %}{% endblock %}
{% block modal-header %}{% trans "Update Metadata" %}{% endblock %}

View File

@ -1,28 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}extra_spec_create_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:flavors:extras:create' flavor.id %}{% endblock %}
{% block modal_id %}extra_spec_create_modal{% endblock %}
{% block modal-header %}{% trans "Create Flavor Extra Spec" %}{% 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 'Create a new "extra spec" key-value pair for a flavor.' %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create" %}" />
<a href="{% url 'horizon:admin:flavors:extras:index' flavor.id %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -1,28 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}extra_spec_edit_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:flavors:extras:edit' flavor.id key %}{% endblock %}
{% block modal_id %}extra_spec_edit_modal{% endblock %}
{% block modal-header %}{% trans "Edit Extra Spec Value" %}: {{ key }}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% blocktrans %}Update the "extra spec" value for &quot;{{ key }}&quot;{% endblocktrans %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Save" %}" />
<a href="{% url 'horizon:admin:flavors:extras:index' flavor.id %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -1,15 +0,0 @@
{% extends "horizon/common/_modal.html" %}
{% load i18n %}
{% load url from future %}
{% block modal_id %}extra_specs_modal{% endblock %}
{% block modal-header %}{% trans "Flavor Extra Specs" %}{% endblock %}
{% block modal-body %}
{{ table.render }}
{% endblock %}
{% block modal-footer %}
<a href="{% url 'horizon:admin:flavors:index' %}" class="btn btn-default secondary cancel close">{% trans "Close" %}</a>
{% endblock %}

View File

@ -1,12 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Flavor Extra Spec" %}{% endblock %}
{% block page_header %}
<h2>{% trans "Flavor" %}: {{flavor.name}} </h2>
{% endblock page_header %}
{% block main %}
{% include "admin/flavors/extras/_create.html" %}
{% endblock %}

View File

@ -1,12 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Edit Flavor Extra Spec" %}{% endblock %}
{% block page_header %}
<h2>{% trans "Flavor" %}: {{flavor.name}} </h2>
{% endblock page_header %}
{% block main %}
{% include "admin/flavors/extras/_edit.html" %}
{% endblock %}

View File

@ -1,12 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Flavor Extra Specs" %}{% endblock %}
{% block page_header %}
<h2>{% trans "Flavor" %}: {{flavor.name}} </h2>
{% endblock page_header %}
{% block main %}
{% include "admin/flavors/extras/_index.html" %}
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Update Flavor Metadata" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Update Flavor Metadata") %}
{% endblock page_header %}
{% block main %}
{% include 'admin/flavors/_update_metadata.html' %}
{% endblock %}

View File

@ -10,6 +10,8 @@
# 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
@ -19,10 +21,9 @@ from openstack_dashboard.test import helpers as test
from novaclient.v1_1 import flavors
from openstack_dashboard.dashboards.admin.flavors import constants
from openstack_dashboard.dashboards.admin.flavors import workflows
INDEX_URL = reverse('horizon:admin:flavors:index')
class FlavorsViewTests(test.BaseAdminViewTests):
@test.create_stubs({api.nova: ('flavor_list',),
@ -33,8 +34,8 @@ class FlavorsViewTests(test.BaseAdminViewTests):
flavors.Flavor.get_keys().MultipleTimes().AndReturn({})
self.mox.ReplayAll()
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'admin/flavors/index.html')
res = self.client.get(reverse(constants.FLAVORS_INDEX_URL))
self.assertTemplateUsed(res, constants.FLAVORS_TEMPLATE_NAME)
self.assertItemsEqual(res.context['table'].data, self.flavors.list())
@ -82,9 +83,9 @@ class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
False])
self.mox.ReplayAll()
url = reverse('horizon:admin:flavors:create')
url = reverse(constants.FLAVORS_CREATE_URL)
res = self.client.get(url)
self.assertTemplateUsed(res, 'admin/flavors/create.html')
self.assertTemplateUsed(res, constants.FLAVORS_CREATE_VIEW_TEMPLATE)
workflow = res.context['workflow']
expected_name = workflows.CreateFlavor.name
self.assertEqual(res.context['workflow'].name, expected_name)
@ -114,11 +115,11 @@ class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
workflow_data = self._get_workflow_data(flavor)
url = reverse('horizon:admin:flavors:create')
url = reverse(constants.FLAVORS_CREATE_URL)
res = self.client.post(url, workflow_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assertRedirectsNoFollow(res, reverse(constants.FLAVORS_INDEX_URL))
@test.create_stubs({api.keystone: ('tenant_list',),
api.nova: ('flavor_list',
@ -146,11 +147,11 @@ class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
workflow_data = self._get_workflow_data(flavor, access=projects)
url = reverse('horizon:admin:flavors:create')
url = reverse(constants.FLAVORS_CREATE_URL)
res = self.client.post(url, workflow_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assertRedirectsNoFollow(res, reverse(constants.FLAVORS_INDEX_URL))
@test.create_stubs({api.keystone: ('tenant_list',),
api.nova: ('flavor_list',)})
@ -169,7 +170,7 @@ class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
workflow_data = self._get_workflow_data(flavor)
url = reverse('horizon:admin:flavors:create')
url = reverse(constants.FLAVORS_CREATE_URL)
res = self.client.post(url, workflow_data)
self.assertFormErrors(res)
@ -195,7 +196,7 @@ class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
# Flavor id already exists.
workflow_data['flavor_id'] = flavor.id
url = reverse('horizon:admin:flavors:create')
url = reverse(constants.FLAVORS_CREATE_URL)
res = self.client.post(url, workflow_data)
self.assertFormErrors(res)
@ -228,12 +229,12 @@ class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
workflow_data = self._get_workflow_data(flavor, access=projects)
url = reverse('horizon:admin:flavors:create')
url = reverse(constants.FLAVORS_CREATE_URL)
res = self.client.post(url, workflow_data)
self.assertNoFormErrors(res)
self.assertMessageCount(error=1, warning=0)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assertRedirectsNoFollow(res, reverse(constants.FLAVORS_INDEX_URL))
@test.create_stubs({api.keystone: ('tenant_list',),
api.nova: ('flavor_list',)})
@ -251,7 +252,7 @@ class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
workflow_data = self._get_workflow_data(flavor)
workflow_data["name"] = ""
url = reverse('horizon:admin:flavors:create')
url = reverse(constants.FLAVORS_CREATE_URL)
res = self.client.post(url, workflow_data)
self.assertFormErrors(res)
@ -278,10 +279,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
# Put all mocks created by mox into replay mode
self.mox.ReplayAll()
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
res = self.client.get(url)
self.assertTemplateUsed(res, 'admin/flavors/update.html')
self.assertTemplateUsed(res, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
workflow = res.context['workflow']
expected_name = workflows.UpdateFlavor.name
@ -314,10 +315,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
self.mox.ReplayAll()
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
res = self.client.get(url)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assertRedirectsNoFollow(res, reverse(constants.FLAVORS_INDEX_URL))
@test.create_stubs({api.keystone: ('tenant_list',),
api.nova: ('flavor_get',
@ -368,10 +369,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
self.mox.ReplayAll()
# run get test
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp, "admin/flavors/update.html")
self.assertTemplateUsed(resp, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
# run post test
workflow_data = {'flavor_id': flavor.id,
@ -385,7 +386,8 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
resp = self.client.post(url, workflow_data)
self.assertNoFormErrors(resp)
self.assertMessageCount(success=1)
self.assertRedirectsNoFollow(resp, INDEX_URL)
self.assertRedirectsNoFollow(resp,
reverse(constants.FLAVORS_INDEX_URL))
@test.create_stubs({api.keystone: ('tenant_list',),
api.nova: ('flavor_get',
@ -438,10 +440,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
self.mox.ReplayAll()
# run get test
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp, "admin/flavors/update.html")
self.assertTemplateUsed(resp, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
# run post test
workflow_data = {'flavor_id': flavor.id,
@ -455,7 +457,8 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
resp = self.client.post(url, workflow_data)
self.assertNoFormErrors(resp)
self.assertMessageCount(success=1)
self.assertRedirectsNoFollow(resp, INDEX_URL)
self.assertRedirectsNoFollow(resp,
reverse(constants.FLAVORS_INDEX_URL))
@test.create_stubs({api.keystone: ('tenant_list',),
api.nova: ('flavor_get',
@ -509,10 +512,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
self.mox.ReplayAll()
# run get test
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp, "admin/flavors/update.html")
self.assertTemplateUsed(resp, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
# run post test
workflow_data = {'flavor_id': flavor.id,
@ -526,7 +529,8 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
resp = self.client.post(url, workflow_data)
self.assertNoFormErrors(resp)
self.assertMessageCount(error=1)
self.assertRedirectsNoFollow(resp, INDEX_URL)
self.assertRedirectsNoFollow(resp,
reverse(constants.FLAVORS_INDEX_URL))
@test.create_stubs({api.keystone: ('tenant_list',),
api.nova: ('flavor_get',
@ -591,10 +595,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
self.mox.ReplayAll()
# run get test
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp, "admin/flavors/update.html")
self.assertTemplateUsed(resp, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
# run post test
data = self._get_workflow_data(new_flavor, access=flavor_projects)
@ -602,7 +606,8 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
resp = self.client.post(url, data)
self.assertNoFormErrors(resp)
self.assertMessageCount(error=1, warning=0)
self.assertRedirectsNoFollow(resp, INDEX_URL)
self.assertRedirectsNoFollow(resp,
reverse(constants.FLAVORS_INDEX_URL))
@test.create_stubs({api.keystone: ('tenant_list',),
api.nova: ('flavor_get',
@ -624,10 +629,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
self.mox.ReplayAll()
# run get test
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp, "admin/flavors/update.html")
self.assertTemplateUsed(resp, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
# run post test
workflow_data = {'flavor_id': flavor.id,
@ -673,10 +678,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
self.mox.ReplayAll()
# get test
url = reverse('horizon:admin:flavors:update', args=[flavor_a.id])
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor_a.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp, "admin/flavors/update.html")
self.assertTemplateUsed(resp, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
# post test
data = {'flavor_id': new_flavor.id,
@ -710,10 +715,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
self.mox.ReplayAll()
# run get test
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp, "admin/flavors/update.html")
self.assertTemplateUsed(resp, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
# run post test
workflow_data = {'flavor_id': flavor.id,
@ -757,3 +762,94 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
data = {'eph_gb': -1}
self.generic_update_flavor_invalid_data_form_fails(override_data=data,
error_msg=error)
class FlavorUpdateMetadataViewTest(test.BaseAdminViewTests):
@test.create_stubs({api.nova: ('flavor_get_extras',),
api.glance: ('metadefs_namespace_list',
'metadefs_namespace_get')})
def test_flavor_metadata_get(self):
# <Flavor: m1.metadata>
flavor = self.flavors.list()[3]
namespaces = self.metadata_defs.list()
api.nova.flavor_get_extras(
IsA(http.HttpRequest),
flavor.id
).AndReturn([flavor.extra_specs])
api.glance.metadefs_namespace_list(
IsA(http.HttpRequest),
filters={
'resource_types': ['OS::Nova::Flavor']
}
).AndReturn((namespaces, False, False))
for namespace in namespaces:
api.glance.metadefs_namespace_get(
IsA(http.HttpRequest),
namespace.namespace,
'OS::Nova::Flavor'
).AndReturn(namespace)
self.mox.ReplayAll()
res = self.client.get(
reverse(
constants.FLAVORS_UPDATE_METADATA_URL,
kwargs={'id': flavor.id}
)
)
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(
res,
constants.FLAVORS_UPDATE_METADATA_TEMPLATE
)
self.assertTemplateUsed(
res,
constants.FLAVORS_UPDATE_METADATA_SUBTEMPLATE
)
self.assertContains(res, 'namespace_1')
self.assertContains(res, 'namespace_2')
self.assertContains(res, 'namespace_3')
self.assertContains(res, 'namespace_4')
@test.create_stubs({api.nova: ('flavor_get_extras',
'flavor_extra_set',
'flavor_extra_delete')})
def test_flavor_metadata_update(self):
# <Flavor: m1.metadata>
flavor = self.flavors.list()[3]
api.nova.flavor_get_extras(
IsA(http.HttpRequest),
flavor.id
).AndReturn([flavor.extra_specs])
api.nova.flavor_extra_set(
IsA(http.HttpRequest),
flavor.id,
{'key_mock': 'value_mock'}
).AndReturn(None)
api.nova.flavor_extra_delete(
IsA(http.HttpRequest),
flavor.id,
[]
).AndReturn(None)
self.mox.ReplayAll()
metadata = [{'value': 'value_mock', 'key': 'key_mock'}]
formData = {'metadata': json.dumps(metadata)}
res = self.client.post(
reverse(
constants.FLAVORS_UPDATE_METADATA_URL,
kwargs={'id': flavor.id}
),
formData
)
self.assertEqual(res.status_code, 302)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, reverse(constants.FLAVORS_INDEX_URL))
self.assertMessageCount(success=1)

View File

@ -16,18 +16,16 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import include # noqa
from django.conf.urls import patterns # noqa
from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.admin.flavors.extras \
import urls as extras_urls
from openstack_dashboard.dashboards.admin.flavors import views
urlpatterns = patterns('openstack_dashboard.dashboards.admin.flavors.views',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<id>[^/]+)/update_metadata/$',
views.UpdateMetadataView.as_view(), name='update_metadata'),
url(r'^(?P<id>[^/]+)/update/$', views.UpdateView.as_view(), name='update'),
url(r'^(?P<id>[^/]+)/extras/', include(extras_urls, namespace='extras')),
)

View File

@ -16,15 +16,21 @@
# 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.utils import memoized
from horizon import workflows
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.flavors \
import forms as project_forms
from openstack_dashboard.dashboards.admin.flavors \
import tables as project_tables
from openstack_dashboard.dashboards.admin.flavors \
@ -78,3 +84,60 @@ class UpdateView(workflows.WorkflowView):
'disk_gb': flavor.disk,
'swap_mb': flavor.swap or 0,
'eph_gb': getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral', None)}
class UpdateMetadataView(forms.ModalFormView):
template_name = "admin/flavors/update_metadata.html"
form_class = project_forms.UpdateMetadataForm
success_url = reverse_lazy('horizon:admin:flavors:index')
def get_initial(self):
extra_specs = self.get_object()
extra_specs_dict = dict((i.key, i.value)
for i in extra_specs)
return {'id': self.kwargs["id"], 'metadata': extra_specs_dict}
def get_context_data(self, **kwargs):
context = super(UpdateMetadataView, self).get_context_data(**kwargs)
extra_specs = self.get_object()
extra_specs_dict = dict((i.key, i.value) for i in extra_specs)
try:
context['existing_metadata'] = json.dumps(extra_specs_dict)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve flavor metadata.'))
resource_type = 'OS::Nova::Flavor'
try:
metadata = {}
# metadefs_namespace_list() returns a tuple with list as 1st elem
metadata["namespaces"] = [
api.glance.metadefs_namespace_get(self.request, x.namespace,
resource_type)
for x in api.glance.metadefs_namespace_list(
self.request,
filters={"resource_types": [resource_type]}
)[0]
]
context['available_metadata'] = json.dumps(metadata)
except Exception:
msg = _('Unable to retrieve available metadata for '
'flavors.')
exceptions.handle(self.request, msg)
context['id'] = self.kwargs['id']
return context
@memoized.memoized_method
def get_object(self):
flavor_id = self.kwargs['id']
try:
extra_specs = api.nova.flavor_get_extras(self.request, flavor_id)
except Exception:
msg = _('Unable to retrieve the flavor metadata.')
exceptions.handle(self.request, msg)
else:
return extra_specs

View File

@ -1281,6 +1281,7 @@ class InstanceTests(helpers.TestCase):
if custom_flavor_sort == 'id':
# Reverse sorted by id
sorted_flavors = (
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'),
('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'),
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'),
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'),
@ -1288,6 +1289,7 @@ class InstanceTests(helpers.TestCase):
elif custom_flavor_sort == 'name':
sorted_flavors = (
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'),
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'),
('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'),
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'),
)
@ -1296,6 +1298,7 @@ class InstanceTests(helpers.TestCase):
('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'),
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'),
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'),
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'),
)
else:
# Default - sorted by RAM
@ -1303,6 +1306,7 @@ class InstanceTests(helpers.TestCase):
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'),
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'),
('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'),
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'),
)
select_options = '\n'.join([

View File

@ -507,22 +507,19 @@ SECURITY_GROUP_RULES = {
},
}
FLAVOR_EXTRA_KEYS = {
'flavor_keys': [
('quota:disk_read_bytes_sec', _('Quota: Read bytes')),
('quota:disk_write_bytes_sec', _('Quota: Write bytes')),
('quota:cpu_quota', _('Quota: CPU')),
('quota:cpu_period', _('Quota: CPU period')),
('quota:vif_inbound_average', _('Quota: Inbound average')),
('quota:vif_outbound_average', _('Quota: Outbound average')),
('hw:cpu_sockets', _('Quota: CPU sockets')),
('hw:cpu_cores', _('Quota: CPU cores')),
('hw:cpu_threads', _('Quota: CPU threads')),
('hw:cpu_max_sockets', _('Quota: Max CPU sockets')),
('hw:cpu_max_cores', _('Quota: Max CPU cores')),
('hw:cpu_max_threads', _('Quota: Max CPU threads')),
]
}
# Deprecation Notice:
#
# The setting FLAVOR_EXTRA_KEYS has been deprecated.
# Please load extra spec metadata into the Glance Metadata Definition Catalog.
#
# The sample quota definitions can be found in:
# <glance_source>/etc/metadefs/compute-quota.json
#
# The metadata definition catalog supports CLI and API:
# $glance --os-image-api-version 2 help md-namespace-import
# $glance-manage db_load_metadefs <directory_with_definition_files>
#
# See Metadata Definitions on: http://docs.openstack.org/developer/glance/
# Indicate to the Sahara data processing service whether or not
# automatic floating IP allocation is in effect. If it is not

View File

@ -436,6 +436,7 @@ def my_custom_sort(flavor):
'm1.secret': 0,
'm1.tiny': 1,
'm1.massive': 2,
'm1.metadata': 3,
}
return sort_order[flavor.name]

View File

@ -200,16 +200,5 @@ POLICY_FILES = {
'compute': 'nova_policy.json'
}
FLAVOR_EXTRA_KEYS = {
'flavor_keys': [
('quota:disk_read_bytes_sec', 'Quota: Read bytes'),
('quota:disk_write_bytes_sec', 'Quota: Write bytes'),
('quota:cpu_quota', 'Quota: CPU'),
('quota:cpu_period', 'Quota: CPU period'),
('quota:vif_inbound_average', 'Quota: Inbound average'),
('quota:vif_outbound_average', 'Quota: Outbound average'),
]
}
# The openstack_auth.user.Token object isn't JSON-serializable ATM
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'

View File

@ -41,6 +41,17 @@ from openstack_dashboard.usage import quotas as usage_quotas
from openstack_dashboard.test.test_data import utils
class FlavorExtraSpecs(dict):
def __repr__(self):
return "<FlavorExtraSpecs %s>" % self._info
def __init__(self, info):
super(FlavorExtraSpecs, self).__init__()
self.__dict__.update(info)
self.update(info)
self._info = info
SERVER_DATA = """
{
"server": {
@ -271,7 +282,19 @@ def data(TEST):
'extra_specs': {},
'os-flavor-access:is_public': False,
'OS-FLV-EXT-DATA:ephemeral': 2048})
TEST.flavors.add(flavor_1, flavor_2, flavor_3)
flavor_4 = flavors.Flavor(flavors.FlavorManager(None),
{'id': "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee",
'name': 'm1.metadata',
'vcpus': 1000,
'disk': 1024,
'ram': 10000,
'swap': 0,
'extra_specs': FlavorExtraSpecs(
{'key': 'key_mock',
'value': 'value_mock'}),
'os-flavor-access:is_public': False,
'OS-FLV-EXT-DATA:ephemeral': 2048})
TEST.flavors.add(flavor_1, flavor_2, flavor_3, flavor_4)
flavor_access_manager = flavor_access.FlavorAccessManager(None)
flavor_access_1 = flavor_access.FlavorAccess(flavor_access_manager,