Allow multiple nodes in a rack
Use a FormsetDataTable in Rack's create and edit workflows to allow creating or updating multiple nodes at once. This is a stopgap solution needed before we completely rework this workflow according to the new wireframes. There is a problem left: * There is currently no way to update the existing nodes in nova baremetal, so editing the fields of existing nodes has no effect. Closes-Bug: #1234245 Implements: blueprint rack-nodes-1-to-many Change-Id: I27f5e9df97af6ba49acf9702fac17a0a883c6930
This commit is contained in:
parent
db5901ae25
commit
d4362da947
88
tuskar_ui/infrastructure/resource_management/nodes/forms.py
Normal file
88
tuskar_ui/infrastructure/resource_management/nodes/forms.py
Normal file
@ -0,0 +1,88 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# 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 django.forms
|
||||
from django.utils.translation import ugettext_lazy as _ # noqa
|
||||
|
||||
import tuskar_ui.forms
|
||||
|
||||
|
||||
class NodeForm(django.forms.Form):
|
||||
id = django.forms.IntegerField(required=False,
|
||||
widget=django.forms.HiddenInput())
|
||||
|
||||
service_host = django.forms.CharField(label=_("Service Host"),
|
||||
widget=django.forms.TextInput(attrs={'class': 'input input-mini'}),
|
||||
required=True)
|
||||
mac_address = django.forms.CharField(label=_("MAC Address"),
|
||||
widget=django.forms.TextInput(attrs={'class': 'input input-mini'}),
|
||||
required=True)
|
||||
|
||||
# Hardware Specifications
|
||||
cpus = django.forms.IntegerField(label=_("CPUs"), required=True,
|
||||
min_value=1, widget=tuskar_ui.forms.NumberInput(attrs={
|
||||
'class': 'input number_input_slim',
|
||||
}))
|
||||
memory_mb = django.forms.IntegerField(label=_("Memory"),
|
||||
required=True, min_value=1, widget=tuskar_ui.forms.NumberInput(attrs={
|
||||
'class': 'input number_input_slim',
|
||||
}))
|
||||
local_gb = django.forms.IntegerField(label=_("Local Disk (GB)"),
|
||||
min_value=1, widget=tuskar_ui.forms.NumberInput(attrs={
|
||||
'class': 'input number_input_slim',
|
||||
}), required=True)
|
||||
|
||||
# Power Management
|
||||
pm_address = django.forms.GenericIPAddressField(
|
||||
widget=django.forms.TextInput(attrs={'class': 'input input-mini'}),
|
||||
label=_("Power Management IP"), required=False)
|
||||
pm_user = django.forms.CharField(label=_("Power Management User"),
|
||||
widget=django.forms.TextInput(attrs={'class': 'input input-mini'}),
|
||||
required=False)
|
||||
pm_password = django.forms.CharField(label=_("Power Management Password"),
|
||||
required=False, widget=django.forms.PasswordInput(render_value=False,
|
||||
attrs={'class': 'input input-mini'}))
|
||||
|
||||
# Access
|
||||
terminal_port = django.forms.IntegerField(label=_("Terminal Port"),
|
||||
required=False, min_value=0, max_value=1024,
|
||||
widget=tuskar_ui.forms.NumberInput(attrs={
|
||||
'class': 'input number_input_slim',
|
||||
}))
|
||||
|
||||
|
||||
class BaseNodeFormSet(django.forms.formsets.BaseFormSet):
|
||||
def clean(self):
|
||||
if any(self.errors):
|
||||
# Don't bother validating the formset unless each form is valid
|
||||
# on its own
|
||||
return
|
||||
unique_fields = ('mac_address', 'pm_address')
|
||||
values = dict((field, {}) for field in unique_fields)
|
||||
for form in self.forms:
|
||||
for field in unique_fields:
|
||||
value = form.cleaned_data.get(field)
|
||||
if not value:
|
||||
continue
|
||||
if value in values[field]:
|
||||
message = _("This value repeats, but it should be unique.")
|
||||
other_form = values[field][value]
|
||||
form._errors[field] = form.error_class([message])
|
||||
other_form._errors[field] = other_form.error_class(
|
||||
[message])
|
||||
values[field][value] = form
|
||||
|
||||
|
||||
NodeFormset = django.forms.formsets.formset_factory(NodeForm, extra=1,
|
||||
can_delete=True, formset=BaseNodeFormSet)
|
@ -17,6 +17,9 @@ from django.utils.translation import ugettext_lazy as _ # noqa
|
||||
from horizon import tables
|
||||
|
||||
from tuskar_ui import api as tuskar
|
||||
from tuskar_ui.infrastructure.resource_management.nodes import forms \
|
||||
as nodes_forms
|
||||
import tuskar_ui.tables
|
||||
|
||||
|
||||
class DeleteNodes(tables.DeleteAction):
|
||||
@ -47,7 +50,7 @@ class NodesTable(tables.DataTable):
|
||||
verbose_name=_("Usage"))
|
||||
|
||||
class Meta:
|
||||
name = "nodes"
|
||||
name = "nodes_table"
|
||||
verbose_name = _("Nodes")
|
||||
table_actions = (DeleteNodes, NodesFilterAction)
|
||||
row_actions = (DeleteNodes,)
|
||||
@ -60,3 +63,32 @@ class UnrackedNodesTable(NodesTable):
|
||||
verbose_name = _("Unracked Nodes")
|
||||
table_actions = ()
|
||||
row_actions = ()
|
||||
|
||||
|
||||
class NodesFormsetTable(tuskar_ui.tables.FormsetDataTable):
|
||||
service_host = tables.Column('service_host', verbose_name=_("Name"))
|
||||
mac_address = tables.Column('mac_address', verbose_name=_("MAC Address"))
|
||||
|
||||
cpus = tables.Column('cpus', verbose_name=_("CPUs"))
|
||||
memory_mb = tables.Column('memory_mb', verbose_name=_("Memory (MB)"))
|
||||
local_gb = tables.Column('local_gb', verbose_name=_("Local Disk (GB)"))
|
||||
|
||||
pm_address = tables.Column('pm_address',
|
||||
verbose_name=_("Power Management IP"))
|
||||
pm_user = tables.Column('pm_user', verbose_name=_("Power Management User"))
|
||||
pm_password = tables.Column('pm_password',
|
||||
verbose_name=_("Power Management Password"))
|
||||
|
||||
terminal_port = tables.Column('terminal_port',
|
||||
verbose_name=_("Terminal Port"))
|
||||
|
||||
# This is needed for the formset with can_delete=True
|
||||
DELETE = tables.Column('DELETE', verbose_name=_("Delete"))
|
||||
|
||||
formset_class = nodes_forms.NodeFormset
|
||||
|
||||
class Meta:
|
||||
name = "nodes"
|
||||
verbose_name = _("Nodes")
|
||||
table_actions = ()
|
||||
multi_select = False
|
||||
|
@ -20,6 +20,7 @@ from horizon import tables
|
||||
|
||||
from tuskar_ui import api as tuskar
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -36,7 +36,7 @@ class NodesTab(tabs.TableTab):
|
||||
slug = "nodes"
|
||||
template_name = "horizon/common/_detail_table.html"
|
||||
|
||||
def get_nodes_data(self):
|
||||
def get_nodes_table_data(self):
|
||||
try:
|
||||
rack = self.tab_group.kwargs['rack']
|
||||
nodes = rack.list_nodes
|
||||
|
@ -26,6 +26,10 @@ class RackViewTests(test.BaseAdminViewTests):
|
||||
index_page = urlresolvers.reverse(
|
||||
'horizon:infrastructure:resource_management:index')
|
||||
|
||||
index_page_racks_tab = (urlresolvers.reverse(
|
||||
'horizon:infrastructure:resource_management:index') +
|
||||
"?tab=resource_management_tabs__racks_tab")
|
||||
|
||||
@test.create_stubs({tuskar.ResourceClass: ('list',)})
|
||||
def test_create_rack_get(self):
|
||||
tuskar.ResourceClass.list(
|
||||
@ -57,14 +61,14 @@ class RackViewTests(test.BaseAdminViewTests):
|
||||
tuskar.BaremetalNode.create(
|
||||
mox.IsA(http.HttpRequest),
|
||||
name='New Node',
|
||||
cpus=u'1',
|
||||
memory_mb=u'1024',
|
||||
local_gb=u'10',
|
||||
cpus=1,
|
||||
memory_mb=1024,
|
||||
local_gb=10,
|
||||
prov_mac_address='aa:bb:cc:dd:ee',
|
||||
pm_address=u'',
|
||||
pm_user=u'',
|
||||
pm_password=u'',
|
||||
terminal_port=u'').AndReturn(node)
|
||||
terminal_port=None).AndReturn(node)
|
||||
tuskar.Rack.create(
|
||||
mox.IsA(http.HttpRequest),
|
||||
name='New Rack',
|
||||
@ -78,14 +82,24 @@ class RackViewTests(test.BaseAdminViewTests):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
data = {'name': 'New Rack', 'resource_class_id': u'1',
|
||||
'location': 'Tokyo', 'subnet': '1.2.3.4',
|
||||
'node_name': 'New Node', 'prov_mac_address': 'aa:bb:cc:dd:ee',
|
||||
'cpus': u'1', 'memory_mb': u'1024', 'local_gb': u'10'}
|
||||
data = {
|
||||
'name': 'New Rack',
|
||||
'resource_class_id': u'1',
|
||||
'location': 'Tokyo',
|
||||
'subnet': '1.2.3.4',
|
||||
'nodes-TOTAL_FORMS': 1,
|
||||
'nodes-INITIAL_FORMS': 0,
|
||||
'nodes-MAX_NUM_FORMS': 1024,
|
||||
'nodes-0-service_host': 'New Node',
|
||||
'nodes-0-mac_address': 'aa:bb:cc:dd:ee',
|
||||
'nodes-0-cpus': u'1',
|
||||
'nodes-0-memory_mb': u'1024',
|
||||
'nodes-0-local_gb': u'10',
|
||||
}
|
||||
url = urlresolvers.reverse('horizon:infrastructure:'
|
||||
'resource_management:racks:create')
|
||||
resp = self.client.post(url, data)
|
||||
self.assertRedirectsNoFollow(resp, self.index_page)
|
||||
self.assertRedirectsNoFollow(resp, self.index_page_racks_tab)
|
||||
|
||||
@test.create_stubs({tuskar.Rack: ('get', 'list_nodes'),
|
||||
tuskar.ResourceClass: ('list',)})
|
||||
@ -94,37 +108,48 @@ class RackViewTests(test.BaseAdminViewTests):
|
||||
|
||||
tuskar.Rack.get(
|
||||
mox.IsA(http.HttpRequest), rack.id).AndReturn(rack)
|
||||
tuskar.Rack.list_nodes = []
|
||||
tuskar.Rack.get(mox.IsA(http.HttpRequest), rack.id).AndReturn(rack)
|
||||
tuskar.ResourceClass.list(
|
||||
mox.IsA(http.HttpRequest)).AndReturn(
|
||||
self.tuskar_resource_classes.list())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
tuskar.Rack.list_nodes = []
|
||||
|
||||
url = urlresolvers.reverse('horizon:infrastructure:'
|
||||
'resource_management:racks:edit',
|
||||
args=[1])
|
||||
'resource_management:racks:edit', args=[1])
|
||||
res = self.client.get(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTemplateUsed(res,
|
||||
'horizon/common/_workflow_base.html')
|
||||
self.assertTemplateUsed(res, 'horizon/common/_workflow_base.html')
|
||||
|
||||
@test.create_stubs({tuskar.Rack: ('get', 'list', 'update',),
|
||||
@test.create_stubs({tuskar.Rack: ('get', 'list', 'update', 'list_nodes'),
|
||||
tuskar.ResourceClass: ('list',)})
|
||||
def test_edit_rack_post(self):
|
||||
rack = self.tuskar_racks.first()
|
||||
|
||||
rack_data = {'name': 'Updated Rack', 'resource_class_id': u'1',
|
||||
'rack_id': u'1', 'location': 'New Location',
|
||||
'subnet': '127.10.10.0/24', 'node_macs': None}
|
||||
rack_data = {
|
||||
'name': 'Updated Rack',
|
||||
'resource_class_id': u'1',
|
||||
'rack_id': u'1',
|
||||
'location': 'New Location',
|
||||
'subnet': '127.10.10.0/24',
|
||||
'nodes': [],
|
||||
}
|
||||
|
||||
data = {'name': 'Updated Rack', 'resource_class_id': u'1',
|
||||
'rack_id': u'1', 'location': 'New Location',
|
||||
'subnet': '127.10.10.0/24', 'node_macs': None,
|
||||
'node_name': 'New Node', 'prov_mac_address': 'aa:bb:cc:dd:ee',
|
||||
'cpus': u'1', 'memory_mb': u'1024', 'local_gb': u'10'}
|
||||
data = {
|
||||
'name': 'Updated Rack',
|
||||
'resource_class_id': u'1',
|
||||
'rack_id': u'1',
|
||||
'location': 'New Location',
|
||||
'subnet': '127.10.10.0/24',
|
||||
'nodes-TOTAL_FORMS': 0,
|
||||
'nodes-INITIAL_FORMS': 0,
|
||||
'nodes-MAX_NUM_FORMS': 1024,
|
||||
}
|
||||
|
||||
tuskar.Rack.get(
|
||||
mox.IsA(http.HttpRequest), rack.id).AndReturn(rack)
|
||||
tuskar.Rack.list_nodes = []
|
||||
tuskar.Rack.get(
|
||||
mox.IsA(http.HttpRequest), rack.id).AndReturn(rack)
|
||||
tuskar.Rack.list(
|
||||
@ -143,7 +168,7 @@ class RackViewTests(test.BaseAdminViewTests):
|
||||
response = self.client.post(url, data)
|
||||
self.assertNoFormErrors(response)
|
||||
self.assertMessageCount(success=1)
|
||||
self.assertRedirectsNoFollow(response, self.index_page)
|
||||
self.assertRedirectsNoFollow(response, self.index_page_racks_tab)
|
||||
|
||||
@test.create_stubs({tuskar.Rack: ('get',)})
|
||||
def test_edit_status_rack_get(self):
|
||||
|
@ -23,55 +23,9 @@ from horizon import workflows
|
||||
import requests
|
||||
|
||||
from tuskar_ui import api as tuskar
|
||||
|
||||
|
||||
class NodeCreateAction(workflows.Action):
|
||||
# node_macs = forms.CharField(label=_("MAC Addresses"),
|
||||
# widget=forms.Textarea(attrs={'rows': 12, 'cols': 20}),
|
||||
# required=False)
|
||||
|
||||
node_name = forms.CharField(label="Name", required=True)
|
||||
prov_mac_address = forms.CharField(label=("MAC Address"),
|
||||
required=True)
|
||||
|
||||
# Hardware Specifications
|
||||
cpus = forms.CharField(label="CPUs", required=True)
|
||||
memory_mb = forms.CharField(label="Memory", required=True)
|
||||
local_gb = forms.CharField(label="Local Disk (GB)", required=True)
|
||||
|
||||
# Power Management
|
||||
pm_address = forms.CharField(label="Power Management IP", required=False)
|
||||
pm_user = forms.CharField(label="Power Management User", required=False)
|
||||
pm_password = forms.CharField(label="Power Management Password",
|
||||
required=False,
|
||||
widget=forms.PasswordInput(
|
||||
render_value=False))
|
||||
|
||||
# Access
|
||||
terminal_port = forms.CharField(label="Terminal Port", required=False)
|
||||
|
||||
class Meta:
|
||||
name = _("Nodes")
|
||||
|
||||
|
||||
# mawagner FIXME - For the demo, all we can really do is edit the one
|
||||
# associated node. That's very much _not_ what this form is actually
|
||||
# about, though.
|
||||
class NodeEditAction(NodeCreateAction):
|
||||
|
||||
class Meta:
|
||||
name = _("Nodes")
|
||||
|
||||
# FIXME: mawagner - This is all for debugging. The idea is to fetch
|
||||
# the first node and display it in the form; the latter part needs
|
||||
# implementation. This also needs error handling; right now for testing
|
||||
# I want to let it fail, but don't commit like that! :)
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(NodeEditAction, self).__init__(request, *args, **kwargs)
|
||||
# TODO(Resolve node edits)
|
||||
#rack_id = self.initial['rack_id']
|
||||
#rack = tuskar.Rack.get(request, rack_id)
|
||||
#nodes = rack.list_nodes
|
||||
from tuskar_ui.infrastructure.resource_management.nodes import tables \
|
||||
as nodes_tables
|
||||
import tuskar_ui.workflows
|
||||
|
||||
|
||||
class RackCreateInfoAction(workflows.Action):
|
||||
@ -82,7 +36,6 @@ class RackCreateInfoAction(workflows.Action):
|
||||
'contain letters, numbers, underscores, '
|
||||
'periods and hyphens.')})
|
||||
location = forms.CharField(label=_("Location"))
|
||||
# see GenericIPAddressField, but not for subnets:
|
||||
subnet = forms.CharField(label=_("IP Subnet"))
|
||||
resource_class_id = forms.ChoiceField(label=_("Resource Class"))
|
||||
|
||||
@ -137,21 +90,52 @@ class EditRackInfo(CreateRackInfo):
|
||||
depends_on = ('rack_id',)
|
||||
|
||||
|
||||
class CreateNodes(workflows.Step):
|
||||
action_class = NodeCreateAction
|
||||
contributes = ('node_name', 'prov_mac_address', 'cpus', 'memory_mb',
|
||||
'local_gb', 'pm_address', 'pm_user', 'pm_password',
|
||||
'terminal_port')
|
||||
class NodeCreateAction(workflows.Action):
|
||||
class Meta:
|
||||
name = _("Create Nodes")
|
||||
help_text = _("Here you can create the nodes for this rack.")
|
||||
|
||||
def get_nodes_data():
|
||||
pass
|
||||
def clean(self):
|
||||
cleaned_data = super(NodeCreateAction, self).clean()
|
||||
table = self.initial.get('_tables', {}).get('nodes')
|
||||
if table:
|
||||
formset = table.get_formset()
|
||||
if formset.is_valid():
|
||||
cleaned_data['nodes'] = [form.cleaned_data
|
||||
for form in formset
|
||||
if form.cleaned_data
|
||||
and not
|
||||
form.cleaned_data.get('DELETE')]
|
||||
else:
|
||||
raise forms.ValidationError(_("Errors in the nodes list."))
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class NodeEditAction(NodeCreateAction):
|
||||
class Meta:
|
||||
name = _("Edit Nodes")
|
||||
help_text = _("Here you can edit the nodes for this rack.")
|
||||
|
||||
|
||||
class CreateNodes(tuskar_ui.workflows.TableStep):
|
||||
action_class = NodeCreateAction
|
||||
contributes = ('nodes',)
|
||||
table_classes = (nodes_tables.NodesFormsetTable,)
|
||||
template_name = (
|
||||
'infrastructure/resource_management/racks/_rack_nodes_step.html')
|
||||
|
||||
def get_nodes_data(self):
|
||||
return []
|
||||
|
||||
|
||||
class EditNodes(CreateNodes):
|
||||
action_class = NodeEditAction
|
||||
depends_on = ('rack_id',)
|
||||
contributes = ('node_macs',)
|
||||
# help_text = _("Editing nodes via textbox is not presently supported.")
|
||||
|
||||
def get_nodes_data(self):
|
||||
rack_id = self.workflow.context['rack_id']
|
||||
rack = tuskar.Rack.get(self.workflow.request, rack_id)
|
||||
return rack.list_nodes
|
||||
|
||||
|
||||
class CreateRack(workflows.Workflow):
|
||||
@ -162,31 +146,75 @@ class CreateRack(workflows.Workflow):
|
||||
success_message = _("Rack created.")
|
||||
failure_message = _("Unable to create rack.")
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
if data['node_name'] is not None:
|
||||
node = tuskar.BaremetalNode.create(
|
||||
request,
|
||||
name=data['node_name'],
|
||||
cpus=data['cpus'],
|
||||
memory_mb=data['memory_mb'],
|
||||
local_gb=data['local_gb'],
|
||||
prov_mac_address=data['prov_mac_address'],
|
||||
pm_address=data['pm_address'],
|
||||
pm_user=data['pm_user'],
|
||||
pm_password=data['pm_password'],
|
||||
terminal_port=data['terminal_port'])
|
||||
if node:
|
||||
node_id = node.id
|
||||
else:
|
||||
node_id = None
|
||||
# FIXME active tabs coflict
|
||||
# When on page with tabs, the workflow with more steps is used,
|
||||
# there is a conflict of active tabs and it always shows the
|
||||
# first tab after an action. So I explicitly specify to what
|
||||
# tab it should redirect after action, until the coflict will
|
||||
# be fixed in Horizon.
|
||||
def get_index_url(self):
|
||||
"""This url is used both as success and failure url"""
|
||||
return "%s?tab=resource_management_tabs__racks_tab" %\
|
||||
urlresolvers.reverse('horizon:infrastructure:resource_management:'
|
||||
'index')
|
||||
|
||||
# Then, register the Rack, including the node if it exists
|
||||
def get_success_url(self):
|
||||
return self.get_index_url()
|
||||
|
||||
def get_failure_url(self):
|
||||
return self.get_index_url()
|
||||
|
||||
def create_or_update_node(self, node_data):
|
||||
"""Creates (if id=='') or updates (otherwise) a node."""
|
||||
if node_data['id'] not in ('', None):
|
||||
node_id = unicode(node_data['id'])
|
||||
# TODO(rdopieralski) there is currently no way to update
|
||||
# a baremetal node
|
||||
#
|
||||
# tuskar.BaremetalNode.update(
|
||||
# self.request,
|
||||
# node_id=node_id,
|
||||
# name=node_data['service_host'],
|
||||
# cpus=node_data['cpus'],
|
||||
# memory_mb=node_data['memory_mb'],
|
||||
# local_gb=node_data['local_gb'],
|
||||
# prov_mac_address=node_data['mac_address'],
|
||||
# pm_address=node_data['pm_address'],
|
||||
# pm_user=node_data['pm_user'],
|
||||
# pm_password=node_data['pm_password'],
|
||||
# terminal_port=node_data['terminal_port'])
|
||||
return node_id
|
||||
else:
|
||||
node = tuskar.BaremetalNode.create(
|
||||
self.request,
|
||||
name=node_data['service_host'],
|
||||
cpus=node_data['cpus'],
|
||||
memory_mb=node_data['memory_mb'],
|
||||
local_gb=node_data['local_gb'],
|
||||
prov_mac_address=node_data['mac_address'],
|
||||
pm_address=node_data['pm_address'],
|
||||
pm_user=node_data['pm_user'],
|
||||
pm_password=node_data['pm_password'],
|
||||
terminal_port=node_data['terminal_port'])
|
||||
return node.id
|
||||
|
||||
def handle(self, request, data):
|
||||
# First, create and/or update nodes
|
||||
node_ids = []
|
||||
for node_data in data['nodes']:
|
||||
try:
|
||||
node_id = self.create_or_update_node(node_data)
|
||||
except Exception:
|
||||
exceptions.handle(self.request, _("Unable to update node."))
|
||||
return False
|
||||
else:
|
||||
node_ids.append({'id': node_id})
|
||||
try:
|
||||
# Then, register the Rack, including the nodes
|
||||
tuskar.Rack.create(request, name=data['name'],
|
||||
resource_class_id=data['resource_class_id'],
|
||||
location=data['location'],
|
||||
subnet=data['subnet'],
|
||||
nodes=[{'id': node_id}])
|
||||
resource_class_id=data['resource_class_id'],
|
||||
location=data['location'], subnet=data['subnet'],
|
||||
nodes=node_ids)
|
||||
|
||||
return True
|
||||
except requests.ConnectionError:
|
||||
@ -208,12 +236,16 @@ class EditRack(CreateRack):
|
||||
failure_message = _("Unable to update rack.")
|
||||
|
||||
def handle(self, request, data):
|
||||
node_ids = [{'id': self.create_or_update_node(node_data)}
|
||||
for node_data in data['nodes']]
|
||||
try:
|
||||
rack_id = self.context['rack_id']
|
||||
data['nodes'] = node_ids
|
||||
tuskar.Rack.update(request, rack_id, data)
|
||||
return True
|
||||
except Exception:
|
||||
exceptions.handle(request, _("Unable to update rack."))
|
||||
return False
|
||||
|
||||
|
||||
class DetailEditRack(EditRack):
|
||||
|
@ -0,0 +1,17 @@
|
||||
<noscript><h3>{{ step }}</h3></noscript>
|
||||
<table class="table-fixed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="actions">
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</td>
|
||||
<td class="help_text">
|
||||
{{ step.get_help_text }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div id="nodes-formset-datatable">
|
||||
{{ nodes_table.render }}
|
||||
</div>
|
@ -39,7 +39,5 @@ urlpatterns = defaults.patterns('',
|
||||
)
|
||||
|
||||
if conf.settings.DEBUG:
|
||||
urlpatterns += defaults.patterns('',
|
||||
defaults.url(r'^qunit$',
|
||||
defaults.include(test_urls, namespace='tests'))
|
||||
)
|
||||
urlpatterns += defaults.patterns('', defaults.url(r'^qunit$',
|
||||
defaults.include(test_urls, namespace='tests')))
|
||||
|
@ -508,3 +508,19 @@ input {
|
||||
color: #3290c0;
|
||||
}
|
||||
}
|
||||
|
||||
#nodes-formset-datatable .datatable tbody {
|
||||
input {
|
||||
padding: 2px 1px;
|
||||
}
|
||||
input.number_input_slim {
|
||||
width: 3em;
|
||||
}
|
||||
td {
|
||||
padding: 2px;
|
||||
text-align: center;
|
||||
a.close {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user