Support adding/removing instance security groups
Changes the edit instance dialog into a workflow, and adds a step "Edit security groups" that reuses the admin:project add/remove user dialog. Changes: - Allow "roles" dropdown to be hidden. - Moved project members template into horizon/common. - Moved workflows.py to workflows/create_instance.py so I could add a workflows/update_instance.py. - Moved hardcoded UI text from js to template/workflow definition. - Made filter-fields use placeholder attributes. - Fixed the #add_remove being added to the URL bar when adding/removing members. - Fixes the project members modal getting stuck displaying the lists from the first invocation of the modal and never updating (try clicking "modify users" for different projects). Implements blueprint add-security-group-to-instance Change-Id: If2939e05c92ac50bfc3c6f0112bdfc1785d9fb4e
This commit is contained in:
parent
7fedbdd8f9
commit
f2fb22cf59
@ -6,12 +6,8 @@ horizon.projects = {
|
||||
roles: [],
|
||||
networks_selected: [],
|
||||
networks_available: [],
|
||||
has_roles: true,
|
||||
default_role_id: "",
|
||||
workflow_loaded: false,
|
||||
no_project_members: gettext('This project currently has no members.'),
|
||||
no_available_users: gettext('No more available users to add.'),
|
||||
no_filter_results: gettext('No users found.'),
|
||||
filter_btn_text: gettext('Filter'),
|
||||
|
||||
/* Parses the form field selector's ID to get either the
|
||||
* role or user id (i.e. returns "id12345" when
|
||||
@ -43,6 +39,7 @@ horizon.projects = {
|
||||
* default role id.
|
||||
**/
|
||||
init_properties: function() {
|
||||
horizon.projects.has_roles = $(".project_membership").data('show-roles') !== "no";
|
||||
horizon.projects.default_role_id = $('#id_default_role').attr('value');
|
||||
horizon.projects.init_user_list();
|
||||
horizon.projects.init_role_list();
|
||||
@ -53,6 +50,7 @@ horizon.projects = {
|
||||
* Initializes an associative array mapping user ids to user names.
|
||||
**/
|
||||
init_user_list: function() {
|
||||
horizon.projects.users = [];
|
||||
_.each($(this.get_role_element("")).find("option"), function (option) {
|
||||
horizon.projects.users[option.value] = option.text;
|
||||
});
|
||||
@ -62,6 +60,7 @@ horizon.projects = {
|
||||
* Initializes an associative array mapping role ids to role names.
|
||||
**/
|
||||
init_role_list: function() {
|
||||
horizon.projects.roles = [];
|
||||
_.each($('label[for^="id_role_"]'), function(role) {
|
||||
var id = horizon.projects.get_field_id($(role).attr('for'));
|
||||
horizon.projects.roles[id] = $(role).text();
|
||||
@ -73,6 +72,7 @@ horizon.projects = {
|
||||
* members for each available role.
|
||||
**/
|
||||
init_current_membership: function() {
|
||||
horizon.projects.current_membership = [];
|
||||
var members_list = [];
|
||||
var role_name, role_id, selected_members;
|
||||
_.each(this.get_role_element(''), function(value, key) {
|
||||
@ -229,8 +229,8 @@ horizon.projects = {
|
||||
var role_id = this.is_project_member(user_id);
|
||||
if (role_id) {
|
||||
$(".project_members").append(this.generate_user_element(user_name, user_id, "-"));
|
||||
var $selected_role = $("li[data-user-id$='" + user_id + "']").siblings('.dropdown').children('.dropdown-toggle').children('span');
|
||||
horizon.projects.set_selected_role($selected_role, role_id);
|
||||
var $selected_role = $("li[data-user-id$='" + user_id + "']").siblings('.dropdown').children('.dropdown-toggle').children('span');
|
||||
horizon.projects.set_selected_role($selected_role, role_id);
|
||||
}
|
||||
else {
|
||||
$(".available_users").append(this.generate_user_element(user_name, user_id, "+"));
|
||||
@ -310,12 +310,15 @@ horizon.projects = {
|
||||
**/
|
||||
update_membership: function() {
|
||||
$(".available_users, .project_members").on('click', ".btn-group a[href='#add_remove']", function (evt) {
|
||||
evt.preventDefault();
|
||||
var available = $(".available_users").has($(this)).length;
|
||||
var user_id = horizon.projects.get_field_id($(this).parent().siblings().attr('data-user-id'));
|
||||
|
||||
if (available) {
|
||||
$(this).text("-");
|
||||
$(this).parent().siblings(".role_options").show();
|
||||
if (horizon.projects.has_roles) {
|
||||
$(this).parent().siblings(".role_options").show();
|
||||
}
|
||||
$(".project_members").append($(this).parent().parent());
|
||||
|
||||
horizon.projects.add_user_to_role(user_id, horizon.projects.default_role_id);
|
||||
@ -337,7 +340,7 @@ horizon.projects = {
|
||||
horizon.projects.detect_no_results();
|
||||
|
||||
// remove input filters
|
||||
$("input.filter").val(horizon.projects.filter_btn_text);
|
||||
$("input.filter").val("");
|
||||
});
|
||||
},
|
||||
|
||||
@ -347,15 +350,9 @@ horizon.projects = {
|
||||
**/
|
||||
detect_no_results: function () {
|
||||
$('.filterable').each( function () {
|
||||
var filter = $(this).find('ul').attr('class'),
|
||||
text;
|
||||
if (filter == 'project_members')
|
||||
text = horizon.projects.no_project_members;
|
||||
else
|
||||
text = horizon.projects.no_available_users;
|
||||
var filter = $(this).find('ul').attr('class');
|
||||
|
||||
if (!$('.' + filter).children('ul').length) {
|
||||
$('#no_' + filter).text(text);
|
||||
$('#no_' + filter).show();
|
||||
$("input[id='" + filter + "']").attr('disabled', 'disabled');
|
||||
}
|
||||
@ -406,7 +403,7 @@ horizon.projects = {
|
||||
// reset lists and input filters
|
||||
horizon.projects.list_filtering();
|
||||
horizon.projects.detect_no_results();
|
||||
$("input.filter").val(horizon.projects.filter_btn_text);
|
||||
$("input.filter").val("");
|
||||
|
||||
// fix styling
|
||||
$(".project_members .btn-group").removeClass('last_stripe');
|
||||
@ -454,13 +451,6 @@ horizon.projects = {
|
||||
// remove previous lists' quicksearch events
|
||||
$('input.filter').unbind();
|
||||
|
||||
// set up what happens on focus of input boxes
|
||||
$("input.filter").on('focus', function() {
|
||||
if ($(this).val() === horizon.projects.filter_btn_text) {
|
||||
$(this).val("");
|
||||
}
|
||||
});
|
||||
|
||||
// set up quicksearch to filter on input
|
||||
$('.filterable').each(function () {
|
||||
var filter = $(this).children().children('ul').attr('class');
|
||||
@ -478,9 +468,6 @@ horizon.projects = {
|
||||
$(this).parent().parent().hide();
|
||||
},
|
||||
'noResults': 'ul#no_' + filter,
|
||||
'onBefore': function () {
|
||||
$('ul#no_' + filter).text(horizon.projects.no_filter_results);
|
||||
},
|
||||
'onAfter': function () {
|
||||
horizon.projects.fix_stripes();
|
||||
},
|
||||
@ -504,8 +491,18 @@ horizon.projects = {
|
||||
**/
|
||||
workflow_init: function(modal) {
|
||||
horizon.projects.generate_networklist_html();
|
||||
if (!horizon.projects.workflow_loaded) {
|
||||
$(modal).find('form').each( function () {
|
||||
|
||||
// fix the dropdown menu overflow issues
|
||||
$(".tab-content, .workflow").addClass("dropdown_fix");
|
||||
|
||||
$(modal).find('form').each( function () {
|
||||
var $form = $(this);
|
||||
|
||||
// Do nothing if this isn't a membership modal
|
||||
if ($form.find('div.project_membership').length == 0) {
|
||||
return; // continue
|
||||
}
|
||||
|
||||
// call the initalization functions
|
||||
horizon.projects.init_properties();
|
||||
horizon.projects.generate_html();
|
||||
@ -514,16 +511,18 @@ horizon.projects = {
|
||||
horizon.projects.add_new_user();
|
||||
|
||||
// initially hide role dropdowns for available users list
|
||||
$(".available_users .role_options").hide();
|
||||
$form.find(".available_users .role_options").hide();
|
||||
|
||||
// fix the dropdown menu overflow issues
|
||||
$(".tab-content, .workflow").addClass("dropdown_fix");
|
||||
// hide the dropdown for members too if we don't need to show it
|
||||
if (!horizon.projects.has_roles) {
|
||||
$form.find(".project_members .role_options").hide();
|
||||
}
|
||||
|
||||
// unfocus filter fields
|
||||
$("#update_project__update_members input").blur();
|
||||
$form.find("#update_project__update_members input").blur();
|
||||
|
||||
// prevent filter inputs from submitting form on 'enter'
|
||||
$('.project_membership').keydown(function(event){
|
||||
$form.find('.project_membership').keydown(function(event){
|
||||
if(event.keyCode == 13) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
@ -534,23 +533,18 @@ horizon.projects = {
|
||||
horizon.projects.add_new_user_styling();
|
||||
horizon.projects.list_filtering();
|
||||
horizon.projects.detect_no_results();
|
||||
horizon.projects.workflow_loaded = true;
|
||||
|
||||
// fix initial striping of rows
|
||||
$('.fake_table').each( function () {
|
||||
$form.find('.fake_table').each( function () {
|
||||
var filter = "." + $(this).attr('id');
|
||||
$(filter + ' .btn-group:even').addClass('dark_stripe');
|
||||
$(filter + ' .btn-group:last').addClass('last_stripe');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
horizon.addInitFunction(function() {
|
||||
$('.btn').on('click', function (evt) {
|
||||
horizon.projects.workflow_loaded = false;
|
||||
});
|
||||
horizon.modals.addModalInitFunction(horizon.projects.workflow_init);
|
||||
});
|
||||
|
@ -0,0 +1,39 @@
|
||||
{% load i18n %}
|
||||
|
||||
<noscript><h3>{{ step }}</h3></noscript>
|
||||
|
||||
<div class="project_membership" data-show-roles="{{ step.show_roles|yesno }}">
|
||||
<div class="header">
|
||||
<div class="help_text">{{ step.help_text }}</div>
|
||||
<div class="left">
|
||||
<div class="fake_table fake_table_header">
|
||||
<span class="users_title">{{ step.available_list_title }}</span>
|
||||
<input type="text" name="available_users_filter" id="available_users" class="filter" placeholder="Filter">
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="fake_table fake_table_header">
|
||||
<span class="users_title">{{ step.members_list_title }}</span>
|
||||
<input type="text" name="project_members_filter" id="project_members" class="filter" placeholder="Filter">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="left filterable">
|
||||
<div class="fake_table" id="available_users">
|
||||
<ul class="available_users"></ul>
|
||||
<ul class="no_results" id="no_available_users"><li>{{ step.no_available_text }}</li></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right filterable">
|
||||
<div class="fake_table" id="project_members">
|
||||
<ul class="project_members"></ul>
|
||||
<ul class="no_results" id="no_project_members"><li>{{ step.no_members_text }}</li></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide">
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</div>
|
@ -1,2 +1,2 @@
|
||||
from .base import Workflow, Step, Action
|
||||
from .base import Workflow, Step, Action, UpdateMembersStep
|
||||
from .views import WorkflowView
|
||||
|
@ -438,6 +438,38 @@ class WorkflowMetaclass(type):
|
||||
return type.__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
class UpdateMembersStep(Step):
|
||||
"""A step that allows a user to add/remove members from a group.
|
||||
|
||||
.. attribute:: show_roles
|
||||
|
||||
Set to False to disable the display of the roles dropdown.
|
||||
|
||||
.. attribute:: available_list_title
|
||||
|
||||
The title used for the available list column.
|
||||
|
||||
.. attribute:: members_list_title
|
||||
|
||||
The title used for the members list column.
|
||||
|
||||
.. attribute:: no_available_text
|
||||
|
||||
The placeholder text used when the available list is empty.
|
||||
|
||||
.. attribute:: no_members_text
|
||||
|
||||
The placeholder text used when the members list is empty.
|
||||
|
||||
"""
|
||||
template_name = "horizon/common/_workflow_step_update_members.html"
|
||||
show_roles = True
|
||||
available_list_title = _("All available")
|
||||
members_list_title = _("Members")
|
||||
no_available_text = _("None available.")
|
||||
no_members_text = _("No members.")
|
||||
|
||||
|
||||
class Workflow(html.HTMLElement):
|
||||
"""
|
||||
A Workflow is a collection of Steps. It's interface is very
|
||||
|
@ -400,6 +400,17 @@ def server_security_groups(request, instance_id):
|
||||
return security_groups
|
||||
|
||||
|
||||
def server_add_security_group(request, instance_id, security_group_name):
|
||||
return novaclient(request).servers.add_security_group(instance_id,
|
||||
security_group_name)
|
||||
|
||||
|
||||
def server_remove_security_group(request, instance_id, security_group_name):
|
||||
return novaclient(request).servers.remove_security_group(
|
||||
instance_id,
|
||||
security_group_name)
|
||||
|
||||
|
||||
def server_pause(request, instance_id):
|
||||
novaclient(request).servers.pause(instance_id)
|
||||
|
||||
|
@ -34,6 +34,10 @@ from openstack_dashboard.dashboards.project.instances.tables import (
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminEditInstance(EditInstance):
|
||||
url = "horizon:admin:instances:update"
|
||||
|
||||
|
||||
class MigrateInstance(tables.BatchAction):
|
||||
name = "migrate"
|
||||
action_present = _("Migrate")
|
||||
@ -112,6 +116,7 @@ class AdminInstancesTable(tables.DataTable):
|
||||
status_columns = ["status", "task"]
|
||||
table_actions = (TerminateInstance,)
|
||||
row_class = AdminUpdateRow
|
||||
row_actions = (ConfirmResize, RevertResize, EditInstance, ConsoleLink,
|
||||
LogLink, CreateSnapshot, TogglePause, ToggleSuspend,
|
||||
MigrateInstance, RebootInstance, TerminateInstance)
|
||||
row_actions = (ConfirmResize, RevertResize, AdminEditInstance,
|
||||
ConsoleLink, LogLink, CreateSnapshot, TogglePause,
|
||||
ToggleSuspend, MigrateInstance, RebootInstance,
|
||||
TerminateInstance)
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
from django.conf.urls.defaults import url, patterns
|
||||
|
||||
from .views import DetailView, AdminIndexView
|
||||
from .views import DetailView, AdminIndexView, AdminUpdateView
|
||||
|
||||
|
||||
INSTANCES = r'^(?P<instance_id>[^/]+)/%s$'
|
||||
@ -28,6 +28,7 @@ INSTANCES = r'^(?P<instance_id>[^/]+)/%s$'
|
||||
|
||||
urlpatterns = patterns('openstack_dashboard.dashboards.admin.instances.views',
|
||||
url(r'^$', AdminIndexView.as_view(), name='index'),
|
||||
url(INSTANCES % 'update', AdminUpdateView.as_view(), name='update'),
|
||||
url(INSTANCES % 'detail', DetailView.as_view(), name='detail'),
|
||||
url(INSTANCES % 'console', 'console', name='console'),
|
||||
url(INSTANCES % 'vnc', 'vnc', name='vnc'),
|
||||
|
@ -31,11 +31,17 @@ from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.admin.instances.tables import \
|
||||
AdminInstancesTable
|
||||
from openstack_dashboard.dashboards.project.instances.views import \
|
||||
console, DetailView, vnc, spice
|
||||
console, DetailView, vnc, spice, UpdateView
|
||||
from openstack_dashboard.dashboards.project.instances.workflows.\
|
||||
update_instance import AdminUpdateInstance
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminUpdateView(UpdateView):
|
||||
workflow_class = AdminUpdateInstance
|
||||
|
||||
|
||||
class AdminIndexView(tables.DataTableView):
|
||||
table_class = AdminInstancesTable
|
||||
template_name = 'admin/instances/index.html'
|
||||
|
@ -169,9 +169,12 @@ class UpdateProjectMembersAction(workflows.Action):
|
||||
slug = "update_members"
|
||||
|
||||
|
||||
class UpdateProjectMembers(workflows.Step):
|
||||
class UpdateProjectMembers(workflows.UpdateMembersStep):
|
||||
action_class = UpdateProjectMembersAction
|
||||
template_name = "admin/projects/_update_members.html"
|
||||
available_list_title = _("All Users")
|
||||
members_list_title = _("Project Members")
|
||||
no_available_text = _("No users found.")
|
||||
no_members_text = _("No users.")
|
||||
|
||||
def contribute(self, data, context):
|
||||
if data:
|
||||
|
@ -1,52 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
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
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UpdateInstance(forms.SelfHandlingForm):
|
||||
tenant_id = forms.CharField(widget=forms.HiddenInput)
|
||||
instance = forms.CharField(widget=forms.HiddenInput)
|
||||
name = forms.CharField(required=True)
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
server = api.nova.server_update(request, data['instance'],
|
||||
data['name'])
|
||||
messages.success(request,
|
||||
_('Instance "%s" updated.') % data['name'])
|
||||
return server
|
||||
except:
|
||||
redirect = reverse("horizon:project:instances:index")
|
||||
exceptions.handle(request,
|
||||
_('Unable to update instance.'),
|
||||
redirect=redirect)
|
@ -201,10 +201,31 @@ class EditInstance(tables.LinkAction):
|
||||
url = "horizon:project:instances:update"
|
||||
classes = ("ajax-modal", "btn-edit")
|
||||
|
||||
def get_link_url(self, project):
|
||||
return self._get_link_url(project, 'instance_info')
|
||||
|
||||
def _get_link_url(self, project, step_slug):
|
||||
base_url = urlresolvers.reverse(self.url, args=[project.id])
|
||||
param = urlencode({"step": step_slug})
|
||||
return "?".join([base_url, param])
|
||||
|
||||
def allowed(self, request, instance):
|
||||
return not is_deleting(instance)
|
||||
|
||||
|
||||
class EditInstanceSecurityGroups(EditInstance):
|
||||
name = "edit_secgroups"
|
||||
verbose_name = _("Edit Security Groups")
|
||||
|
||||
def get_link_url(self, project):
|
||||
return self._get_link_url(project, 'update_security_groups')
|
||||
|
||||
def allowed(self, request, instance=None):
|
||||
return (instance.status in ACTIVE_STATES and
|
||||
not is_deleting(instance) and
|
||||
request.user.tenant_id == instance.tenant_id)
|
||||
|
||||
|
||||
class CreateSnapshot(tables.LinkAction):
|
||||
name = "snapshot"
|
||||
verbose_name = _("Create Snapshot")
|
||||
@ -449,5 +470,6 @@ class InstancesTable(tables.DataTable):
|
||||
row_actions = (ConfirmResize, RevertResize, CreateSnapshot,
|
||||
SimpleAssociateIP, AssociateIP,
|
||||
SimpleDisassociateIP, EditInstance,
|
||||
ConsoleLink, LogLink, TogglePause, ToggleSuspend,
|
||||
RebootInstance, TerminateInstance)
|
||||
EditInstanceSecurityGroups, ConsoleLink, LogLink,
|
||||
TogglePause, ToggleSuspend, RebootInstance,
|
||||
TerminateInstance)
|
||||
|
@ -1,24 +0,0 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form_id %}update_instance_form{% endblock %}
|
||||
{% block form_action %}{% url horizon:project:instances:update instance_id %}{% endblock %}
|
||||
|
||||
{% block modal-header %}{% trans "Edit Instance" %}{% 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 "You may update the editable properties of your instance here." %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Save Changes" %}" />
|
||||
<a href="{% url horizon:project:instances:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -1,11 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Update Instance" %}{% endblock %}
|
||||
{% block title %}{% trans "Edit Instance" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Update Instance") %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Edit Instance") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/instances/_update.html' %}
|
||||
{% include 'horizon/common/_workflow.html' %}
|
||||
{% endblock %}
|
||||
|
@ -600,11 +600,20 @@ class InstanceTests(test.TestCase):
|
||||
res = self.client.post(url, formData)
|
||||
self.assertRedirects(res, redir_url)
|
||||
|
||||
@test.create_stubs({api.nova: ('server_get',)})
|
||||
instance_update_get_stubs = {
|
||||
api.nova: ('server_get',
|
||||
'security_group_list',
|
||||
'server_security_groups',)}
|
||||
|
||||
@test.create_stubs(instance_update_get_stubs)
|
||||
def test_instance_update_get(self):
|
||||
server = self.servers.first()
|
||||
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id).AndReturn(server)
|
||||
api.nova.security_group_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn([])
|
||||
api.nova.server_security_groups(IsA(http.HttpRequest),
|
||||
server.id).AndReturn([])
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -613,7 +622,7 @@ class InstanceTests(test.TestCase):
|
||||
|
||||
self.assertTemplateUsed(res, 'project/instances/update.html')
|
||||
|
||||
@test.create_stubs({api.nova: ('server_get',)})
|
||||
@test.create_stubs(instance_update_get_stubs)
|
||||
def test_instance_update_get_server_get_exception(self):
|
||||
server = self.servers.first()
|
||||
|
||||
@ -628,45 +637,121 @@ class InstanceTests(test.TestCase):
|
||||
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({api.nova: ('server_get', 'server_update')})
|
||||
def _instance_update_post(self, server_id, server_name, secgroups):
|
||||
formData = {'name': server_name,
|
||||
'default_role': 'member',
|
||||
'role_member': secgroups}
|
||||
url = reverse('horizon:project:instances:update',
|
||||
args=[server_id])
|
||||
return self.client.post(url, formData)
|
||||
|
||||
instance_update_post_stubs = {
|
||||
api.nova: ('server_get', 'server_update',
|
||||
'security_group_list',
|
||||
'server_security_groups',
|
||||
'server_add_security_group',
|
||||
'server_remove_security_group')}
|
||||
|
||||
@test.create_stubs(instance_update_post_stubs)
|
||||
def test_instance_update_post(self):
|
||||
server = self.servers.first()
|
||||
secgroups = self.security_groups.list()[:3]
|
||||
new_name = 'manuel'
|
||||
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id).AndReturn(server)
|
||||
api.nova.security_group_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn(secgroups)
|
||||
api.nova.server_security_groups(IsA(http.HttpRequest),
|
||||
server.id).AndReturn([])
|
||||
|
||||
api.nova.server_update(IsA(http.HttpRequest),
|
||||
server.id,
|
||||
server.name).AndReturn(server)
|
||||
new_name).AndReturn(server)
|
||||
api.nova.server_security_groups(IsA(http.HttpRequest),
|
||||
server.id).AndReturn([])
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
formData = {'method': 'UpdateInstance',
|
||||
'instance': server.id,
|
||||
'name': server.name,
|
||||
'tenant_id': self.tenant.id}
|
||||
url = reverse('horizon:project:instances:update',
|
||||
args=[server.id])
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
res = self._instance_update_post(server.id, new_name, [])
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({api.nova: ('server_get', 'server_update')})
|
||||
@test.create_stubs(instance_update_post_stubs)
|
||||
def test_instance_update_secgroup_post(self):
|
||||
server = self.servers.first()
|
||||
secgroups = self.security_groups.list()[:3]
|
||||
|
||||
server_groups = [secgroups[0], secgroups[1]]
|
||||
wanted_groups = [secgroups[1].name, secgroups[2].name]
|
||||
expect_add = secgroups[2].name
|
||||
expect_rm = secgroups[0].name
|
||||
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id).AndReturn(server)
|
||||
api.nova.security_group_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn(secgroups)
|
||||
api.nova.server_security_groups(IsA(http.HttpRequest),
|
||||
server.id).AndReturn(server_groups)
|
||||
|
||||
api.nova.server_update(IsA(http.HttpRequest),
|
||||
server.id,
|
||||
server.name).AndReturn(server)
|
||||
api.nova.server_security_groups(IsA(http.HttpRequest),
|
||||
server.id).AndReturn(server_groups)
|
||||
|
||||
api.nova.server_add_security_group(IsA(http.HttpRequest),
|
||||
server.id,
|
||||
expect_add).AndReturn(server)
|
||||
api.nova.server_remove_security_group(IsA(http.HttpRequest),
|
||||
server.id,
|
||||
expect_rm).AndReturn(server)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self._instance_update_post(server.id, server.name, wanted_groups)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs(instance_update_post_stubs)
|
||||
def test_instance_update_post_api_exception(self):
|
||||
server = self.servers.first()
|
||||
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id).AndReturn(server)
|
||||
api.nova.security_group_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn([])
|
||||
api.nova.server_security_groups(IsA(http.HttpRequest),
|
||||
server.id).AndReturn([])
|
||||
|
||||
api.nova.server_update(IsA(http.HttpRequest), server.id, server.name) \
|
||||
.AndRaise(self.exceptions.nova)
|
||||
api.nova.server_security_groups(IsA(http.HttpRequest),
|
||||
server.id) \
|
||||
.AndRaise(self.exceptions.nova)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
formData = {'method': 'UpdateInstance',
|
||||
'instance': server.id,
|
||||
'name': server.name,
|
||||
'tenant_id': self.tenant.id}
|
||||
url = reverse('horizon:project:instances:update',
|
||||
args=[server.id])
|
||||
res = self.client.post(url, formData)
|
||||
res = self._instance_update_post(server.id, server.name, [])
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs(instance_update_post_stubs)
|
||||
def test_instance_update_post_secgroup_api_exception(self):
|
||||
server = self.servers.first()
|
||||
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id).AndReturn(server)
|
||||
api.nova.security_group_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn([])
|
||||
api.nova.server_security_groups(IsA(http.HttpRequest),
|
||||
server.id).AndReturn([])
|
||||
|
||||
api.nova.server_update(IsA(http.HttpRequest),
|
||||
server.id,
|
||||
server.name).AndReturn(server)
|
||||
api.nova.server_security_groups(IsA(http.HttpRequest),
|
||||
server.id) \
|
||||
.AndRaise(self.exceptions.nova)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self._instance_update_post(server.id, server.name, [])
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({api.nova: ('flavor_list',
|
||||
|
@ -36,10 +36,9 @@ from horizon import tables
|
||||
from horizon import workflows
|
||||
|
||||
from openstack_dashboard import api
|
||||
from .forms import UpdateInstance
|
||||
from .tabs import InstanceDetailTabs
|
||||
from .tables import InstancesTable
|
||||
from .workflows import LaunchInstance
|
||||
from .workflows import LaunchInstance, UpdateInstance
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -135,10 +134,9 @@ def spice(request, instance_id):
|
||||
exceptions.handle(request, msg, redirect=redirect)
|
||||
|
||||
|
||||
class UpdateView(forms.ModalFormView):
|
||||
form_class = UpdateInstance
|
||||
class UpdateView(workflows.WorkflowView):
|
||||
workflow_class = UpdateInstance
|
||||
template_name = 'project/instances/update.html'
|
||||
context_object_name = 'instance'
|
||||
success_url = reverse_lazy("horizon:project:instances:index")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -158,9 +156,10 @@ class UpdateView(forms.ModalFormView):
|
||||
return self._object
|
||||
|
||||
def get_initial(self):
|
||||
return {'instance': self.kwargs['instance_id'],
|
||||
'tenant_id': self.request.user.tenant_id,
|
||||
'name': getattr(self.get_object(), 'name', '')}
|
||||
initial = super(UpdateView, self).get_initial()
|
||||
initial.update({'instance_id': self.kwargs['instance_id'],
|
||||
'name': getattr(self.get_object(), 'name', '')})
|
||||
return initial
|
||||
|
||||
|
||||
class DetailView(tabs.TabView):
|
||||
|
@ -0,0 +1,2 @@
|
||||
from create_instance import *
|
||||
from update_instance import *
|
@ -0,0 +1,178 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.utils.translation import ugettext as _
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import workflows
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.api import cinder, nova
|
||||
from openstack_dashboard.api.base import is_service_enabled
|
||||
|
||||
|
||||
INDEX_URL = "horizon:projects:instances:index"
|
||||
ADD_USER_URL = "horizon:projects:instances:create_user"
|
||||
|
||||
|
||||
class UpdateInstanceSecurityGroupsAction(workflows.Action):
|
||||
default_role = forms.CharField(required=False)
|
||||
role_member = forms.MultipleChoiceField(required=False)
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(UpdateInstanceSecurityGroupsAction, self).__init__(request,
|
||||
*args,
|
||||
**kwargs)
|
||||
err_msg = _('Unable to retrieve security group list. '
|
||||
'Please try again later.')
|
||||
context = args[0]
|
||||
instance_id = context.get('instance_id', '')
|
||||
|
||||
self.fields['default_role'].initial = 'member'
|
||||
|
||||
# Get list of available security groups
|
||||
all_groups = []
|
||||
try:
|
||||
all_groups = api.nova.security_group_list(request)
|
||||
except:
|
||||
exceptions.handle(request, err_msg)
|
||||
groups_list = [(group.name, group.name) for group in all_groups]
|
||||
|
||||
instance_groups = []
|
||||
try:
|
||||
instance_groups = api.nova.server_security_groups(request,
|
||||
instance_id)
|
||||
except Exception:
|
||||
exceptions.handle(request, err_msg)
|
||||
self.fields['role_member'].choices = groups_list
|
||||
self.fields['role_member'].initial = [group.name
|
||||
for group in instance_groups]
|
||||
|
||||
def handle(self, request, data):
|
||||
instance_id = data['instance_id']
|
||||
|
||||
# update instance security groups
|
||||
wanted_groups = set(data['wanted_groups'])
|
||||
try:
|
||||
current_groups = api.nova.server_security_groups(request,
|
||||
instance_id)
|
||||
except:
|
||||
exceptions.handle(request, _("Couldn't get current security group "
|
||||
"list for instance %s."
|
||||
% instance_id))
|
||||
return False
|
||||
|
||||
current_group_names = set(map(lambda g: g.name, current_groups))
|
||||
groups_to_add = wanted_groups - current_group_names
|
||||
groups_to_remove = current_group_names - wanted_groups
|
||||
|
||||
num_groups_to_modify = len(groups_to_add | groups_to_remove)
|
||||
try:
|
||||
for group in groups_to_add:
|
||||
api.nova.server_add_security_group(request,
|
||||
instance_id,
|
||||
group)
|
||||
num_groups_to_modify -= 1
|
||||
for group in groups_to_remove:
|
||||
api.nova.server_remove_security_group(request,
|
||||
instance_id,
|
||||
group)
|
||||
num_groups_to_modify -= 1
|
||||
except Exception:
|
||||
exceptions.handle(request, _('Failed to modify %d instance '
|
||||
'security groups.'
|
||||
% num_groups_to_modify))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
class Meta:
|
||||
name = _("Security Groups")
|
||||
slug = "update_security_groups"
|
||||
|
||||
|
||||
class UpdateInstanceSecurityGroups(workflows.UpdateMembersStep):
|
||||
action_class = UpdateInstanceSecurityGroupsAction
|
||||
help_text = _("From here you can add and remove security groups to "
|
||||
"this project from the list of available security groups.")
|
||||
available_list_title = _("All Security Groups")
|
||||
members_list_title = _("Instance Security Groups")
|
||||
no_available_text = _("No security groups found.")
|
||||
no_members_text = _("No security groups enabled.")
|
||||
show_roles = False
|
||||
depends_on = ("instance_id",)
|
||||
contributes = ("wanted_groups",)
|
||||
|
||||
def contribute(self, data, context):
|
||||
request = self.workflow.request
|
||||
if data:
|
||||
context["wanted_groups"] = request.POST.getlist("role_member")
|
||||
return context
|
||||
|
||||
|
||||
class UpdateInstanceInfoAction(workflows.Action):
|
||||
name = forms.CharField(required=True)
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
api.nova.server_update(request,
|
||||
data['instance_id'],
|
||||
data['name'])
|
||||
except:
|
||||
exceptions.handle(request, ignore=True)
|
||||
return False
|
||||
return True
|
||||
|
||||
class Meta:
|
||||
name = _("Info")
|
||||
slug = 'instance_info'
|
||||
help_text = _("From here you can edit the instance details.")
|
||||
|
||||
|
||||
class UpdateInstanceInfo(workflows.Step):
|
||||
action_class = UpdateInstanceInfoAction
|
||||
depends_on = ("instance_id",)
|
||||
contributes = ("name",)
|
||||
|
||||
|
||||
class UpdateInstance(workflows.Workflow):
|
||||
slug = "update_instance"
|
||||
name = _("Edit Instance")
|
||||
finalize_button_name = _("Save")
|
||||
success_message = _('Modified instance "%s".')
|
||||
failure_message = _('Unable to modify instance "%s".')
|
||||
success_url = "horizon:project:instances:index"
|
||||
default_steps = (UpdateInstanceInfo,
|
||||
UpdateInstanceSecurityGroups)
|
||||
|
||||
def format_status_message(self, message):
|
||||
return message % self.context.get('name', 'unknown instance')
|
||||
|
||||
|
||||
# NOTE(kspear): nova doesn't support instance security group management
|
||||
# by an admin. This isn't really the place for this code,
|
||||
# but the other ways of special-casing this are even messier.
|
||||
class AdminUpdateInstance(UpdateInstance):
|
||||
success_url = "horizon:admin:instances:index"
|
||||
default_steps = (UpdateInstanceInfo,)
|
@ -1416,7 +1416,7 @@ label.log-length {
|
||||
.no_results {
|
||||
border: 1px solid #DDD;
|
||||
padding: 10px;
|
||||
color: #08C;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Role dropdown menus */
|
||||
|
@ -243,6 +243,12 @@ def data(TEST):
|
||||
"id": 2,
|
||||
"name": u"other_group",
|
||||
"description": u"Not default."})
|
||||
sec_group_3 = sec_groups.SecurityGroup(sg_manager,
|
||||
{"rules": [],
|
||||
"tenant_id": TEST.tenant.id,
|
||||
"id": 3,
|
||||
"name": u"another_group",
|
||||
"description": u"Not default."})
|
||||
|
||||
rule = {'id': 1,
|
||||
'ip_protocol': u"tcp",
|
||||
@ -278,7 +284,7 @@ def data(TEST):
|
||||
|
||||
sec_group_1.rules = [rule_obj]
|
||||
sec_group_2.rules = [rule_obj]
|
||||
TEST.security_groups.add(sec_group_1, sec_group_2)
|
||||
TEST.security_groups.add(sec_group_1, sec_group_2, sec_group_3)
|
||||
|
||||
# Security Group Rules
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user