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:
Jan Provaznik 2013-07-01 09:48:00 +02:00 committed by Tomas Sedovic
parent f9e15d2e6c
commit eec0013c5b
8 changed files with 273 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -47,3 +47,7 @@ svg {
.circles_chart_time_picker {
float: right;
}
.csv_rack_table {
padding-top: 40px;
}