Adds racks import from CSV
CSV file format: rack name,resoruce class name,subnet,region,list of mac addresses example:: Rack1,rclass1,192.168.111.0/24,regionX,f0:dd:f1:da:f9:b5 f2🇩🇪f1:da:f9:66 f2🇩🇪ff:da:f9:67 Change-Id: Ifd15266bb694d04d2e5951dcfc90bd5bca36bd36
This commit is contained in:
parent
f9e15d2e6c
commit
eec0013c5b
@ -0,0 +1,118 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
import re
|
||||
|
||||
from django.core import validators
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.forms import ValidationError
|
||||
from django import template
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
import csv
|
||||
import base64
|
||||
import StringIO
|
||||
import logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UploadRack(forms.SelfHandlingForm):
|
||||
csv_file = forms.FileField(label=_("Choose CSV File"),
|
||||
help_text=("CSV file with rack definitions"),
|
||||
required=False)
|
||||
uploaded_data = forms.CharField(widget=forms.HiddenInput(),
|
||||
required=False)
|
||||
|
||||
def clean_csv_file(self):
|
||||
csv_file = self.cleaned_data['csv_file']
|
||||
data = csv_file.read() if csv_file else None
|
||||
|
||||
if 'upload' in self.request.POST:
|
||||
if not csv_file:
|
||||
raise ValidationError(_('CSV file not set.'))
|
||||
else:
|
||||
try:
|
||||
CSVRack.from_str(data)
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to parse rack CSV file.")
|
||||
raise ValidationError(_('Failed to parse CSV file.'))
|
||||
return data
|
||||
|
||||
def clean_uploaded_data(self):
|
||||
data = self.cleaned_data['uploaded_data']
|
||||
if 'add_racks' in self.request.POST:
|
||||
if not data:
|
||||
raise ValidationError(_('Upload CSV file first'))
|
||||
elif 'upload' in self.request.POST:
|
||||
# reset obsolete uploaded data
|
||||
self.data['uploaded_data'] = None
|
||||
return data
|
||||
|
||||
def handle(self, request, data):
|
||||
if 'upload' in self.request.POST:
|
||||
# if upload button was pressed, stay on the same page
|
||||
# but show content of the CSV file in table
|
||||
racks_str = self.cleaned_data['csv_file']
|
||||
self.initial['racks'] = CSVRack.from_str(racks_str)
|
||||
self.data['uploaded_data'] = base64.b64encode(racks_str)
|
||||
return False
|
||||
else:
|
||||
fails = []
|
||||
successes = []
|
||||
racks_str = self.cleaned_data['uploaded_data']
|
||||
racks = CSVRack.from_str(base64.b64decode(racks_str))
|
||||
# get the resource class ids by resource class names
|
||||
rclass_ids = {rc.name: rc.id for rc in
|
||||
api.management.ResourceClass.list(request)}
|
||||
for rack in racks:
|
||||
try:
|
||||
r = api.management.Rack.create(request, rack.name,
|
||||
rclass_ids[rack.resource_class], rack.region,
|
||||
rack.subnet)
|
||||
api.management.Rack.register_nodes(r, rack.nodes)
|
||||
successes.append(rack.name)
|
||||
except:
|
||||
LOG.exception("Exception in processing rack CSV file.")
|
||||
fails.append(rack.name)
|
||||
if successes:
|
||||
messages.success(request,
|
||||
_('Added %d racks.') % len(successes))
|
||||
if fails:
|
||||
messages.error(request,
|
||||
_('Failed to add following racks: %s') %
|
||||
(',').join(fails))
|
||||
return True
|
||||
|
||||
|
||||
class CSVRack:
|
||||
def __init__(self, **kwargs):
|
||||
self.id = kwargs['id']
|
||||
self.name = kwargs['name']
|
||||
self.resource_class = kwargs['resource_class']
|
||||
self.region = kwargs['region']
|
||||
self.subnet = kwargs['subnet']
|
||||
self.nodes = kwargs['nodes']
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, csv_str):
|
||||
racks = []
|
||||
csvreader = csv.reader(StringIO.StringIO(csv_str), delimiter=',')
|
||||
for row in csvreader:
|
||||
# ignore empty rows
|
||||
if not row:
|
||||
continue
|
||||
racks.append(cls(id=row[0],
|
||||
name=row[0],
|
||||
resource_class=row[1],
|
||||
subnet=row[2],
|
||||
region=row[3],
|
||||
nodes=row[4].split()))
|
||||
return racks
|
||||
|
||||
def nodes_count(self):
|
||||
return len(self.nodes)
|
@ -38,6 +38,13 @@ class CreateRack(tables.LinkAction):
|
||||
classes = ("ajax-modal", "btn-create")
|
||||
|
||||
|
||||
class UploadRack(tables.LinkAction):
|
||||
name = "upload"
|
||||
verbose_name = _("Upload Rack")
|
||||
url = "horizon:infrastructure:resource_management:racks:upload"
|
||||
classes = ("ajax-modal", "btn-upload")
|
||||
|
||||
|
||||
class EditRack(tables.LinkAction):
|
||||
name = "edit"
|
||||
verbose_name = _("Edit Rack")
|
||||
@ -66,5 +73,17 @@ class RacksTable(tables.DataTable):
|
||||
class Meta:
|
||||
name = "racks"
|
||||
verbose_name = _("Racks")
|
||||
table_actions = (CreateRack, DeleteRacks, RacksFilterAction)
|
||||
table_actions = (UploadRack, CreateRack, DeleteRacks,
|
||||
RacksFilterAction)
|
||||
row_actions = (EditRack, DeleteRacks)
|
||||
|
||||
|
||||
class UploadRacksTable(tables.DataTable):
|
||||
name = tables.Column("name")
|
||||
subnet = tables.Column("subnet")
|
||||
nodes_count = tables.Column("nodes_count")
|
||||
#region = tables.Column("region")
|
||||
|
||||
class Meta:
|
||||
name = "uploaded_racks"
|
||||
verbose_name = " "
|
||||
|
@ -13,8 +13,11 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
from openstack_dashboard.test import helpers as test
|
||||
from openstack_dashboard import api
|
||||
from mox import IsA
|
||||
from mox import IsA, IgnoreArg
|
||||
from django import http
|
||||
import tempfile
|
||||
import base64
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
|
||||
|
||||
class ResourceViewTests(test.BaseAdminViewTests):
|
||||
@ -79,3 +82,63 @@ class ResourceViewTests(test.BaseAdminViewTests):
|
||||
url = reverse('horizon:infrastructure:resource_management:index')
|
||||
result = self.client.post(url, data)
|
||||
self.assertRedirectsNoFollow(result, self.index_page)
|
||||
|
||||
def test_upload_rack_get(self):
|
||||
url = reverse('horizon:infrastructure:resource_management:'
|
||||
'racks:upload')
|
||||
resource = self.client.get(url)
|
||||
|
||||
self.assertEqual(resource.status_code, 200)
|
||||
self.assertTemplateUsed(resource,
|
||||
'infrastructure/resource_management/racks/upload.html')
|
||||
|
||||
def test_upload_rack_upload(self):
|
||||
csv_data = 'Rack1,rclass1,192.168.111.0/24,regionX,f0:dd:f1:da:f9:b5 '\
|
||||
'f2:de:f1:da:f9:66 f2:de:ff:da:f9:67'
|
||||
temp_file = tempfile.TemporaryFile()
|
||||
temp_file.write(csv_data)
|
||||
temp_file.flush()
|
||||
temp_file.seek(0)
|
||||
|
||||
data = {'csv_file': temp_file, 'upload': '1'}
|
||||
url = reverse('horizon:infrastructure:resource_management:'
|
||||
'racks:upload')
|
||||
resp = self.client.post(url, data)
|
||||
self.assertTemplateUsed(resp,
|
||||
'infrastructure/resource_management/racks/upload.html')
|
||||
self.assertNoFormErrors(resp)
|
||||
self.assertEqual(resp.context['form']['uploaded_data'].value(),
|
||||
base64.b64encode(csv_data))
|
||||
|
||||
def test_upload_rack_upload_with_error(self):
|
||||
data = {'upload': '1'}
|
||||
url = reverse('horizon:infrastructure:resource_management:'
|
||||
'racks:upload')
|
||||
resp = self.client.post(url, data)
|
||||
self.assertTemplateUsed(resp,
|
||||
'infrastructure/resource_management/racks/upload.html')
|
||||
self.assertFormErrors(resp, 1)
|
||||
self.assertEqual(resp.context['form']['uploaded_data'].value(),
|
||||
None)
|
||||
|
||||
@test.create_stubs({api.management.Rack: ('create', 'register_nodes'),
|
||||
api.management.ResourceClass: ('list',)})
|
||||
def test_upload_rack_create(self):
|
||||
api.management.Rack.create(IsA(http.request.HttpRequest), 'Rack1',
|
||||
'1', 'regionX', '192.168.111.0/24').AndReturn(None)
|
||||
api.management.Rack.register_nodes(IgnoreArg(),
|
||||
IgnoreArg()).AndReturn(None)
|
||||
api.management.ResourceClass.list(
|
||||
IsA(http.request.HttpRequest)).AndReturn(
|
||||
self.management_resource_classes.list())
|
||||
self.mox.ReplayAll()
|
||||
csv_data = 'Rack1,rclass1,192.168.111.0/24,regionX,f0:dd:f1:da:f9:b5 '\
|
||||
'f2:de:f1:da:f9:66 f2:de:ff:da:f9:67'
|
||||
|
||||
data = {'uploaded_data': base64.b64encode(csv_data), 'add_racks': '1'}
|
||||
url = reverse('horizon:infrastructure:resource_management:'
|
||||
'racks:upload')
|
||||
resp = self.client.post(url, data)
|
||||
self.assertRedirectsNoFollow(resp, self.index_page)
|
||||
self.assertMessageCount(success=1)
|
||||
self.assertMessageCount(error=0)
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
from django.conf.urls import patterns, url, include
|
||||
|
||||
from .views import CreateView, EditView, DetailView, UsageDataView
|
||||
from .views import CreateView, UploadView, EditView, DetailView, UsageDataView
|
||||
|
||||
|
||||
RACKS = r'^(?P<rack_id>[^/]+)/%s$'
|
||||
@ -24,6 +24,7 @@ VIEW_MOD = 'openstack_dashboard.dashboards.infrastructure.' \
|
||||
|
||||
urlpatterns = patterns(VIEW_MOD,
|
||||
url(r'^create/$', CreateView.as_view(), name='create'),
|
||||
url(r'^upload/$', UploadView.as_view(), name='upload'),
|
||||
url(r'^usage_data$', UsageDataView.as_view(), name='usage_data'),
|
||||
url(RACKS % 'edit/', EditView.as_view(), name='edit'),
|
||||
url(RACKS % 'detail', DetailView.as_view(), name='detail'),
|
||||
|
@ -28,11 +28,15 @@ from django.views.generic import View
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import tabs
|
||||
from horizon import forms
|
||||
from horizon import workflows
|
||||
from .workflows import (CreateRack, EditRack)
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
from .forms import UploadRack
|
||||
from .tabs import RackDetailTabs
|
||||
from .tables import UploadRacksTable
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -45,6 +49,19 @@ class CreateView(workflows.WorkflowView):
|
||||
pass
|
||||
|
||||
|
||||
class UploadView(forms.ModalFormView):
|
||||
form_class = UploadRack
|
||||
template_name = 'infrastructure/resource_management/racks/upload.html'
|
||||
success_url = reverse_lazy(
|
||||
'horizon:infrastructure:resource_management:index')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UploadView, self).get_context_data(**kwargs)
|
||||
context['racks_table'] = UploadRacksTable(
|
||||
self.request, kwargs['form'].initial.get('racks', []))
|
||||
return context
|
||||
|
||||
|
||||
class EditView(workflows.WorkflowView):
|
||||
workflow_class = EditRack
|
||||
|
||||
|
@ -0,0 +1,37 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}upload_rack_form{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:infrastructure:resource_management:racks:upload' %}{% endblock %}
|
||||
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
|
||||
|
||||
{% block modal_id %}upload_rack_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Upload Rack" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div>
|
||||
<fieldset>
|
||||
<span class="control-group clearfix error">
|
||||
{% if form.csv_file.errors %}
|
||||
<div class="error help-inline">{{ form.csv_file.errors }}</div>
|
||||
{% endif %}
|
||||
</span>
|
||||
{{ form.csv_file }}
|
||||
<input class="btn btn-primary pull-right always-enabled" type="submit" name=upload value="{% trans "Upload File" %}" />
|
||||
<div class="help-block">
|
||||
CSV file format:<br>rack name,resource class name,subnet,region,list of mac addresses separated by space
|
||||
</div>
|
||||
{{ form.uploaded_data }}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="csv_rack_table">
|
||||
{% trans "Uploaded racks, which you are going to add to the system:" %}
|
||||
{{ racks_table.render }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right always-enabled" type="submit" name=add_racks value="{% trans "Add Racks" %}" {% if not form.uploaded_data.value %}disabled{% endif %}/>
|
||||
<a href="{% url 'horizon:infrastructure:resource_management:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'infrastructure/base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Upload Rack" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Upload Rack") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include "infrastructure/resource_management/racks/_upload.html" %}
|
||||
{% endblock %}
|
@ -47,3 +47,7 @@ svg {
|
||||
.circles_chart_time_picker {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.csv_rack_table {
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user