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:
Kieran Spear 2013-01-29 13:51:06 +11:00
parent 7fedbdd8f9
commit f2fb22cf59
20 changed files with 467 additions and 160 deletions

View File

@ -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);
});

View File

@ -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>

View File

@ -1,2 +1,2 @@
from .base import Workflow, Step, Action
from .base import Workflow, Step, Action, UpdateMembersStep
from .views import WorkflowView

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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'),

View File

@ -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'

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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',

View File

@ -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):

View File

@ -0,0 +1,2 @@
from create_instance import *
from update_instance import *

View File

@ -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,)

View File

@ -1416,7 +1416,7 @@ label.log-length {
.no_results {
border: 1px solid #DDD;
padding: 10px;
color: #08C;
opacity: 0.5;
}
/* Role dropdown menus */

View File

@ -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