summaryrefslogtreecommitdiffstats
path: root/dashboard/src
diff options
context:
space:
mode:
Diffstat (limited to 'dashboard/src')
-rw-r--r--dashboard/src/api/migrations/0007_auto_20190417_1511.py25
-rw-r--r--dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py20
-rw-r--r--dashboard/src/api/migrations/0008_auto_20190419_1414.py28
-rw-r--r--dashboard/src/api/migrations/0009_merge_20190508_1317.py14
-rw-r--r--dashboard/src/api/models.py151
-rw-r--r--dashboard/src/api/urls.py4
-rw-r--r--dashboard/src/api/views.py14
-rw-r--r--dashboard/src/booking/forms.py12
-rw-r--r--dashboard/src/booking/migrations/0005_booking_idf.py18
-rw-r--r--dashboard/src/booking/migrations/0006_booking_opnfv_config.py20
-rw-r--r--dashboard/src/booking/models.py4
-rw-r--r--dashboard/src/booking/quick_deployer.py103
-rw-r--r--dashboard/src/dashboard/views.py2
-rw-r--r--dashboard/src/resource_inventory/admin.py5
-rw-r--r--dashboard/src/resource_inventory/idf_templater.py151
-rw-r--r--dashboard/src/resource_inventory/migrations/0009_auto_20190315_1757.py73
-rw-r--r--dashboard/src/resource_inventory/migrations/0010_auto_20190430_1405.py54
-rw-r--r--dashboard/src/resource_inventory/models.py77
-rw-r--r--dashboard/src/resource_inventory/pdf_templater.py53
-rw-r--r--dashboard/src/resource_inventory/resource_manager.py79
-rw-r--r--dashboard/src/resource_inventory/urls.py5
-rw-r--r--dashboard/src/resource_inventory/views.py17
-rw-r--r--dashboard/src/templates/base.html1
-rw-r--r--dashboard/src/templates/booking/booking_table.html2
-rw-r--r--dashboard/src/templates/booking/quick_deploy.html24
-rw-r--r--dashboard/src/templates/booking/steps/booking_meta.html9
-rw-r--r--dashboard/src/templates/config_bundle/steps/assign_host_roles.html22
-rw-r--r--dashboard/src/templates/config_bundle/steps/assign_network_roles.html22
-rw-r--r--dashboard/src/templates/config_bundle/steps/config_software.html50
-rw-r--r--dashboard/src/templates/config_bundle/steps/define_software.html129
-rw-r--r--dashboard/src/templates/config_bundle/steps/pick_installer.html32
-rw-r--r--dashboard/src/templates/config_bundle/steps/table_formset.html63
-rw-r--r--dashboard/src/templates/dashboard/idf.yaml26
-rw-r--r--dashboard/src/templates/dashboard/lab_detail.html1
-rw-r--r--dashboard/src/templates/resource/hostprofile_detail.html116
-rw-r--r--dashboard/src/templates/resource/hosts.html2
-rw-r--r--dashboard/src/templates/resource/steps/meta_info.html2
-rw-r--r--dashboard/src/templates/resource/steps/pod_definition.html146
-rw-r--r--dashboard/src/templates/workflow/viewport-base.html29
-rw-r--r--dashboard/src/workflow/forms.py53
-rw-r--r--dashboard/src/workflow/models.py159
-rw-r--r--dashboard/src/workflow/opnfv_workflow.py327
-rw-r--r--dashboard/src/workflow/resource_bundle_workflow.py214
-rw-r--r--dashboard/src/workflow/snapshot_workflow.py8
-rw-r--r--dashboard/src/workflow/sw_bundle_workflow.py193
-rw-r--r--dashboard/src/workflow/workflow_factory.py15
46 files changed, 1845 insertions, 729 deletions
diff --git a/dashboard/src/api/migrations/0007_auto_20190417_1511.py b/dashboard/src/api/migrations/0007_auto_20190417_1511.py
new file mode 100644
index 0000000..e7d2c59
--- /dev/null
+++ b/dashboard/src/api/migrations/0007_auto_20190417_1511.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.1 on 2019-04-17 15:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0006_auto_20190313_1729'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='opnfvapiconfig',
+ name='idf',
+ field=models.CharField(default='', max_length=100),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='opnfvapiconfig',
+ name='pdf',
+ field=models.CharField(default='', max_length=100),
+ preserve_default=False,
+ ),
+ ]
diff --git a/dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py b/dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py
new file mode 100644
index 0000000..46f3631
--- /dev/null
+++ b/dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.1 on 2019-05-01 18:53
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0010_auto_20190430_1405'),
+ ('api', '0006_auto_20190313_1729'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='opnfvapiconfig',
+ name='opnfv_config',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.OPNFVConfig'),
+ ),
+ ]
diff --git a/dashboard/src/api/migrations/0008_auto_20190419_1414.py b/dashboard/src/api/migrations/0008_auto_20190419_1414.py
new file mode 100644
index 0000000..03c3865
--- /dev/null
+++ b/dashboard/src/api/migrations/0008_auto_20190419_1414.py
@@ -0,0 +1,28 @@
+# Generated by Django 2.1 on 2019-04-19 14:14
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0009_auto_20190315_1757'),
+ ('api', '0007_auto_20190417_1511'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='BridgeConfig',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('interfaces', models.ManyToManyField(to='resource_inventory.Interface')),
+ ('opnfv_config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.OPNFVConfig')),
+ ],
+ ),
+ migrations.AddField(
+ model_name='opnfvapiconfig',
+ name='bridge_config',
+ field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.BridgeConfig'),
+ ),
+ ]
diff --git a/dashboard/src/api/migrations/0009_merge_20190508_1317.py b/dashboard/src/api/migrations/0009_merge_20190508_1317.py
new file mode 100644
index 0000000..1a34380
--- /dev/null
+++ b/dashboard/src/api/migrations/0009_merge_20190508_1317.py
@@ -0,0 +1,14 @@
+# Generated by Django 2.1 on 2019-05-08 13:17
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0008_auto_20190419_1414'),
+ ('api', '0007_opnfvapiconfig_opnfv_config'),
+ ]
+
+ operations = [
+ ]
diff --git a/dashboard/src/api/models.py b/dashboard/src/api/models.py
index f8b8f89..1f708ae 100644
--- a/dashboard/src/api/models.py
+++ b/dashboard/src/api/models.py
@@ -12,6 +12,7 @@ from django.contrib.auth.models import User
from django.db import models
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404
+from django.urls import reverse
import json
import uuid
@@ -23,8 +24,12 @@ from resource_inventory.models import (
Host,
Image,
Interface,
- RemoteInfo
+ HostOPNFVConfig,
+ RemoteInfo,
+ OPNFVConfig
)
+from resource_inventory.idf_templater import IDFTemplater
+from resource_inventory.pdf_templater import PDFTemplater
class JobStatus(object):
@@ -86,8 +91,23 @@ class LabManager(object):
remote_info.save()
host.remote_management = remote_info
host.save()
+ booking = Booking.objects.get(resource=host.bundle)
+ self.update_xdf(booking)
return {"status": "success"}
+ def update_xdf(self, booking):
+ booking.pdf = PDFTemplater.makePDF(booking)
+ booking.idf = IDFTemplater().makeIDF(booking)
+ booking.save()
+
+ def get_pdf(self, booking_id):
+ booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
+ return booking.pdf
+
+ def get_idf(self, booking_id):
+ booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
+ return booking.idf
+
def get_profile(self):
prof = {}
prof['name'] = self.lab.name
@@ -341,25 +361,71 @@ class TaskConfig(models.Model):
self.delta = '{}'
+class BridgeConfig(models.Model):
+ """
+ Displays mapping between jumphost interfaces and
+ bridges
+ """
+ interfaces = models.ManyToManyField(Interface)
+ opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.CASCADE)
+
+ def to_dict(self):
+ d = {}
+ hid = self.interfaces.first().host.labid
+ d[hid] = {}
+ for interface in self.interfaces.all():
+ d[hid][interface.mac_address] = []
+ for vlan in interface.config.all():
+ network_role = self.opnfv_model.networks().filter(network=vlan.network)
+ bridge = IDFTemplater.bridge_names[network_role.name]
+ br_config = {
+ "vlan_id": vlan.vlan_id,
+ "tagged": vlan.tagged,
+ "bridge": bridge
+ }
+ d[hid][interface.mac_address].append(br_config)
+ return d
+
+ def to_json(self):
+ return json.dumps(self.to_dict())
+
+
class OpnfvApiConfig(models.Model):
installer = models.CharField(max_length=200)
scenario = models.CharField(max_length=300)
roles = models.ManyToManyField(Host)
+ # pdf and idf are url endpoints, not the actual file
+ pdf = models.CharField(max_length=100)
+ idf = models.CharField(max_length=100)
+ bridge_config = models.OneToOneField(BridgeConfig, on_delete=models.CASCADE, null=True)
delta = models.TextField()
+ opnfv_config = models.ForeignKey(OPNFVConfig, null=True, on_delete=models.SET_NULL)
def to_dict(self):
d = {}
+ if not self.opnfv_config:
+ return d
if self.installer:
d['installer'] = self.installer
if self.scenario:
d['scenario'] = self.scenario
+ if self.pdf:
+ d['pdf'] = self.pdf
+ if self.idf:
+ d['idf'] = self.idf
+ if self.bridge_config:
+ d['bridged_interfaces'] = self.bridge_config.to_dict()
hosts = self.roles.all()
if hosts.exists():
d['roles'] = []
- for host in self.roles.all():
- d['roles'].append({host.labid: host.config.opnfvRole.name})
+ for host in hosts:
+ d['roles'].append({
+ host.labid: self.opnfv_config.host_opnfv_config.get(
+ host_config__pk=host.config.pk
+ ).role.name
+ })
return d
@@ -378,6 +444,16 @@ class OpnfvApiConfig(models.Model):
d['scenario'] = scenario
self.delta = json.dumps(d)
+ def set_xdf(self, booking, update_delta=True):
+ kwargs = {'lab_name': booking.lab.name, 'booking_id': booking.id}
+ self.pdf = reverse('get-pdf', kwargs=kwargs)
+ self.idf = reverse('get-idf', kwargs=kwargs)
+ if update_delta:
+ d = json.loads(self.delta)
+ d['pdf'] = self.pdf
+ d['idf'] = self.idf
+ self.delta = json.dumps(d)
+
def add_role(self, host):
self.roles.add(host)
d = json.loads(self.delta)
@@ -615,6 +691,7 @@ class SnapshotConfig(TaskConfig):
if not self.delta:
self.delta = self.to_json()
self.save()
+
d = json.loads(self.delta)
return d
@@ -765,14 +842,12 @@ class JobFactory(object):
net_relation.status = JobStatus.NEW
# re-apply ssh access after host is reset
- ssh_relation = AccessRelation.objects.get(job=job, config__access_type="ssh")
- ssh_relation.status = JobStatus.NEW
+ for relation in AccessRelation.objects.filter(job=job, config__access_type="ssh"):
+ relation.status = JobStatus.NEW
+ relation.save()
- # save them all at once to reduce the chance
- # of a lab polling and only seeing partial change
hardware_relation.save()
net_relation.save()
- ssh_relation.save()
@classmethod
def makeSnapshotTask(cls, image, booking, host):
@@ -808,7 +883,7 @@ class JobFactory(object):
job=job
)
cls.makeSoftware(
- hosts=hosts,
+ booking=booking,
job=job
)
all_users = list(booking.collaborators.all())
@@ -899,28 +974,42 @@ class JobFactory(object):
network_config.save()
@classmethod
- def makeSoftware(cls, hosts=[], job=Job()):
- def init_config(host):
- opnfv_config = OpnfvApiConfig()
- if host is not None:
- opnfv = host.config.bundle.opnfv_config.first()
- opnfv_config.installer = opnfv.installer.name
- opnfv_config.scenario = opnfv.scenario.name
- opnfv_config.save()
- return opnfv_config
-
+ def make_bridge_config(cls, booking):
+ if booking.resource.hosts.count() < 2:
+ return None
try:
- host = None
- if len(hosts) > 0:
- host = hosts[0]
- opnfv_config = init_config(host)
-
- for host in hosts:
- opnfv_config.roles.add(host)
- software_config = SoftwareConfig.objects.create(opnfv=opnfv_config)
- software_config.save()
- software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
- software_relation.save()
- return software_relation
+ jumphost_config = HostOPNFVConfig.objects.filter(
+ role__name__iexact="jumphost"
+ )
+ jumphost = Host.objects.get(
+ bundle=booking.resource,
+ config=jumphost_config.host_config
+ )
except Exception:
return None
+ br_config = BridgeConfig.objects.create(opnfv_config=booking.opnfv_config)
+ for iface in jumphost.interfaces.all():
+ br_config.interfaces.add(iface)
+ return br_config
+
+ @classmethod
+ def makeSoftware(cls, booking=None, job=Job()):
+
+ if not booking.opnfv_config:
+ return None
+
+ opnfv_api_config = OpnfvApiConfig.objects.create(
+ opnfv_config=booking.opnfv_config,
+ installer=booking.opnfv_config.installer.name,
+ scenario=booking.opnfv_config.scenario.name,
+ bridge_config=cls.make_bridge_config(booking)
+ )
+
+ opnfv_api_config.set_xdf(booking, False)
+ opnfv_api_config.save()
+
+ for host in booking.resource.hosts.all():
+ opnfv_api_config.roles.add(host)
+ software_config = SoftwareConfig.objects.create(opnfv=opnfv_api_config)
+ software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
+ return software_relation
diff --git a/dashboard/src/api/urls.py b/dashboard/src/api/urls.py
index d18a04d..d1f772a 100644
--- a/dashboard/src/api/urls.py
+++ b/dashboard/src/api/urls.py
@@ -41,6 +41,8 @@ from api.views import (
done_jobs,
update_host_bmc,
lab_host,
+ get_pdf,
+ get_idf,
GenerateTokenView
)
@@ -55,6 +57,8 @@ urlpatterns = [
path('labs/<slug:lab_name>/inventory', lab_inventory),
path('labs/<slug:lab_name>/hosts/<slug:host_id>', lab_host),
path('labs/<slug:lab_name>/hosts/<slug:host_id>/bmc', update_host_bmc),
+ path('labs/<slug:lab_name>/booking/<slug:booking_id>/pdf', get_pdf, name="get-pdf"),
+ path('labs/<slug:lab_name>/booking/<slug:booking_id>/idf', get_idf, name="get-idf"),
path('labs/<slug:lab_name>/jobs/<int:job_id>', specific_job),
path('labs/<slug:lab_name>/jobs/<int:job_id>/<slug:task_id>', specific_task),
path('labs/<slug:lab_name>/jobs/new', new_jobs),
diff --git a/dashboard/src/api/views.py b/dashboard/src/api/views.py
index 2ae1ac5..fb28958 100644
--- a/dashboard/src/api/views.py
+++ b/dashboard/src/api/views.py
@@ -13,7 +13,7 @@ from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.views import View
-from django.http.response import JsonResponse
+from django.http.response import JsonResponse, HttpResponse
from rest_framework import viewsets
from rest_framework.authtoken.models import Token
from django.views.decorators.csrf import csrf_exempt
@@ -64,6 +64,18 @@ def lab_host(request, lab_name="", host_id=""):
return JsonResponse(lab_manager.update_host(host_id, request.POST), safe=False)
+def get_pdf(request, lab_name="", booking_id=""):
+ lab_token = request.META.get('HTTP_AUTH_TOKEN')
+ lab_manager = LabManagerTracker.get(lab_name, lab_token)
+ return HttpResponse(lab_manager.get_pdf(booking_id), content_type="text/plain")
+
+
+def get_idf(request, lab_name="", booking_id=""):
+ lab_token = request.META.get('HTTP_AUTH_TOKEN')
+ lab_manager = LabManagerTracker.get(lab_name, lab_token)
+ return HttpResponse(lab_manager.get_idf(booking_id), content_type="text/plain")
+
+
def lab_status(request, lab_name=""):
lab_token = request.META.get('HTTP_AUTH_TOKEN')
lab_manager = LabManagerTracker.get(lab_name, lab_token)
diff --git a/dashboard/src/booking/forms.py b/dashboard/src/booking/forms.py
index 7ba5af0..de427ab 100644
--- a/dashboard/src/booking/forms.py
+++ b/dashboard/src/booking/forms.py
@@ -8,7 +8,6 @@
##############################################################################
import django.forms as forms
from django.forms.widgets import NumberInput
-from django.db.models import Q
from workflow.forms import (
SearchableSelectMultipleWidget,
@@ -22,7 +21,6 @@ from resource_inventory.models import Image, Installer, Scenario
class QuickBookingForm(forms.Form):
purpose = forms.CharField(max_length=1000)
project = forms.CharField(max_length=400)
- image = forms.ModelChoiceField(queryset=Image.objects.all())
hostname = forms.CharField(max_length=400)
installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=False)
@@ -40,14 +38,12 @@ class QuickBookingForm(forms.Form):
elif data and "users" in data:
chosen_users = data.getlist("users")
- if user:
- self.image = forms.ModelChoiceField(queryset=Image.objects.filter(
- Q(public=True) | Q(owner=user)), required=False)
- else:
- self.image = forms.ModelChoiceField(queryset=Image.objects.all(), required=False)
-
super(QuickBookingForm, self).__init__(data=data, **kwargs)
+ self.fields["image"] = forms.ModelChoiceField(
+ Image.objects.filter(public=True) | Image.objects.filter(owner=user)
+ )
+
self.fields['users'] = forms.CharField(
widget=SearchableSelectMultipleWidget(
attrs=self.build_search_widget_attrs(chosen_users, default_user=default_user)
diff --git a/dashboard/src/booking/migrations/0005_booking_idf.py b/dashboard/src/booking/migrations/0005_booking_idf.py
new file mode 100644
index 0000000..31e9170
--- /dev/null
+++ b/dashboard/src/booking/migrations/0005_booking_idf.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1 on 2019-04-12 19:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('booking', '0004_auto_20190124_1700'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='booking',
+ name='idf',
+ field=models.TextField(blank=True, default=''),
+ ),
+ ]
diff --git a/dashboard/src/booking/migrations/0006_booking_opnfv_config.py b/dashboard/src/booking/migrations/0006_booking_opnfv_config.py
new file mode 100644
index 0000000..e5ffc71
--- /dev/null
+++ b/dashboard/src/booking/migrations/0006_booking_opnfv_config.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.1 on 2019-05-01 18:02
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0010_auto_20190430_1405'),
+ ('booking', '0005_booking_idf'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='booking',
+ name='opnfv_config',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.OPNFVConfig'),
+ ),
+ ]
diff --git a/dashboard/src/booking/models.py b/dashboard/src/booking/models.py
index 8612abd..9836730 100644
--- a/dashboard/src/booking/models.py
+++ b/dashboard/src/booking/models.py
@@ -9,7 +9,7 @@
##############################################################################
-from resource_inventory.models import ResourceBundle, ConfigBundle
+from resource_inventory.models import ResourceBundle, ConfigBundle, OPNFVConfig
from account.models import Lab
from django.contrib.auth.models import User
from django.db import models
@@ -29,9 +29,11 @@ class Booking(models.Model):
ext_count = models.IntegerField(default=2)
resource = models.ForeignKey(ResourceBundle, on_delete=models.SET_NULL, null=True)
config_bundle = models.ForeignKey(ConfigBundle, on_delete=models.SET_NULL, null=True)
+ opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.SET_NULL, null=True)
project = models.CharField(max_length=100, default="", blank=True, null=True)
lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL)
pdf = models.TextField(blank=True, default="")
+ idf = models.TextField(blank=True, default="")
class Meta:
db_table = 'booking'
diff --git a/dashboard/src/booking/quick_deployer.py b/dashboard/src/booking/quick_deployer.py
index f076a2e..763c8a0 100644
--- a/dashboard/src/booking/quick_deployer.py
+++ b/dashboard/src/booking/quick_deployer.py
@@ -22,7 +22,6 @@ from resource_inventory.models import (
Image,
GenericResourceBundle,
ConfigBundle,
- Vlan,
Host,
HostProfile,
HostConfiguration,
@@ -30,7 +29,11 @@ from resource_inventory.models import (
GenericHost,
GenericInterface,
OPNFVRole,
- OPNFVConfig
+ OPNFVConfig,
+ Network,
+ NetworkConnection,
+ NetworkRole,
+ HostOPNFVConfig,
)
from resource_inventory.resource_manager import ResourceManager
from resource_inventory.pdf_templater import PDFTemplater
@@ -90,6 +93,10 @@ class NoRemainingPublicNetwork(Exception):
pass
+class BookingPermissionException(Exception):
+ pass
+
+
def parse_host_field(host_field_contents):
host_json = json.loads(host_field_contents)
lab_dict = host_json['labs'][0]
@@ -179,18 +186,30 @@ def generate_hostconfig(generic_host, image, config_bundle):
hconf = HostConfiguration()
hconf.host = generic_host
hconf.image = image
-
- opnfvrole = OPNFVRole.objects.get(name="Jumphost")
- if not opnfvrole:
- raise OPNFVRoleDNE("No jumphost role was found.")
-
- hconf.opnfvRole = opnfvrole
hconf.bundle = config_bundle
+ hconf.is_head_node = True
hconf.save()
return hconf
+def generate_hostopnfv(hostconfig, opnfvconfig):
+ config = HostOPNFVConfig()
+ role = None
+ try:
+ role = OPNFVRole.objects.get(name="Jumphost")
+ except Exception:
+ role = OPNFVRole.objects.create(
+ name="Jumphost",
+ description="Single server jumphost role"
+ )
+ config.role = role
+ config.host_config = hostconfig
+ config.opnfv_config = opnfvconfig
+ config.save()
+ return config
+
+
def generate_resource_bundle(generic_resource_bundle, config_bundle): # warning: requires cleanup
try:
resource_manager = ResourceManager.getInstance()
@@ -226,6 +245,20 @@ def check_invariants(request, **kwargs):
raise BookingLengthException("Booking must be between 1 and 21 days long")
+def configure_networking(grb, config):
+ # create network
+ net = Network.objects.create(name="public", bundle=grb, is_public=True)
+ # connect network to generic host
+ grb.getHosts()[0].generic_interfaces.first().connections.add(
+ NetworkConnection.objects.create(network=net, vlan_is_tagged=False)
+ )
+ # asign network role
+ role = NetworkRole.objects.create(name="public", network=net)
+ opnfv_config = config.opnfv_config.first()
+ if opnfv_config:
+ opnfv_config.networks.add(role)
+
+
def create_from_form(form, request):
quick_booking_id = str(uuid.uuid4())
@@ -246,62 +279,56 @@ def create_from_form(form, request):
data['host_profile'] = host_profile
check_invariants(request, **data)
+ # check booking privileges
+ if Booking.objects.filter(owner=request.user, end__gt=timezone.now()).count() >= 3 and not request.user.userprofile.booking_privledge:
+ raise BookingPermissionException("You do not have permission to have more than 3 bookings at a time.")
+
check_available_matching_host(lab, host_profile) # requires cleanup if failure after this point
grbundle = generate_grb(request.user, lab, quick_booking_id)
-
gresource = generate_gresource(grbundle, hostname)
-
ghost = generate_ghost(gresource, host_profile)
-
cbundle = generate_config_bundle(request.user, quick_booking_id, grbundle)
+ hconf = generate_hostconfig(ghost, image, cbundle)
# if no installer provided, just create blank host
+ opnfv_config = None
if installer:
- generate_opnfvconfig(scenario, installer, cbundle)
-
- generate_hostconfig(ghost, image, cbundle)
+ opnfv_config = generate_opnfvconfig(scenario, installer, cbundle)
+ generate_hostopnfv(hconf, opnfv_config)
# construct generic interfaces
for interface_profile in host_profile.interfaceprofile.all():
generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
generic_interface.save()
- # get vlan, assign to first interface
- publicnetwork = lab.vlan_manager.get_public_vlan()
- if not publicnetwork:
- raise NoRemainingPublicNetwork("No public networks were available for your pod")
- publicvlan = publicnetwork.vlan
- lab.vlan_manager.reserve_public_vlan(publicvlan)
-
- vlan = Vlan.objects.create(vlan_id=publicvlan, tagged=False, public=True)
- vlan.save()
-
- ghost.generic_interfaces.first().vlans.add(vlan)
- ghost.generic_interfaces.first().save()
+ configure_networking(grbundle, cbundle)
# generate resource bundle
resource_bundle = generate_resource_bundle(grbundle, cbundle)
# generate booking
- booking = Booking()
- booking.purpose = purpose_field
- booking.project = project_field
- booking.lab = lab
- booking.owner = request.user
- booking.start = timezone.now()
- booking.end = timezone.now() + timedelta(days=int(length))
- booking.resource = resource_bundle
- booking.pdf = PDFTemplater.makePDF(booking.resource)
- booking.config_bundle = cbundle
- booking.save()
+ booking = Booking.objects.create(
+ purpose=purpose_field,
+ project=project_field,
+ lab=lab,
+ owner=request.user,
+ start=timezone.now(),
+ end=timezone.now() + timedelta(days=int(length)),
+ resource=resource_bundle,
+ config_bundle=cbundle,
+ opnfv_config=opnfv_config
+ )
+ booking.pdf = PDFTemplater.makePDF(booking)
+
users_field = users_field[2:-2]
if users_field: # may be empty after split, if no collaborators entered
users_field = json.loads(users_field)
for collaborator in users_field:
user = User.objects.get(id=collaborator['id'])
booking.collaborators.add(user)
- booking.save()
+
+ booking.save()
# generate job
JobFactory.makeCompleteJob(booking)
diff --git a/dashboard/src/dashboard/views.py b/dashboard/src/dashboard/views.py
index c4a6685..aaad7ab 100644
--- a/dashboard/src/dashboard/views.py
+++ b/dashboard/src/dashboard/views.py
@@ -46,7 +46,7 @@ def lab_detail_view(request, lab_name):
'title': "Lab Overview",
'lab': lab,
'hostprofiles': lab.hostprofiles.all(),
- 'images': images
+ 'images': images,
}
)
diff --git a/dashboard/src/resource_inventory/admin.py b/dashboard/src/resource_inventory/admin.py
index e063cc0..7ff510b 100644
--- a/dashboard/src/resource_inventory/admin.py
+++ b/dashboard/src/resource_inventory/admin.py
@@ -32,7 +32,8 @@ from resource_inventory.models import (
OPNFVConfig,
OPNFVRole,
Image,
- HostConfiguration
+ HostConfiguration,
+ RemoteInfo
)
profiles = [HostProfile, InterfaceProfile, DiskProfile, CpuProfile, RamProfile]
@@ -47,6 +48,6 @@ physical = [Host, Interface, Network, Vlan, ResourceBundle]
admin.site.register(physical)
-config = [Scenario, Installer, Opsys, ConfigBundle, OPNFVConfig, OPNFVRole, Image, HostConfiguration]
+config = [Scenario, Installer, Opsys, ConfigBundle, OPNFVConfig, OPNFVRole, Image, HostConfiguration, RemoteInfo]
admin.site.register(config)
diff --git a/dashboard/src/resource_inventory/idf_templater.py b/dashboard/src/resource_inventory/idf_templater.py
new file mode 100644
index 0000000..bf6eda0
--- /dev/null
+++ b/dashboard/src/resource_inventory/idf_templater.py
@@ -0,0 +1,151 @@
+##############################################################################
+# Copyright (c) 2019 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.template.loader import render_to_string
+
+from account.models import PublicNetwork
+
+from resource_inventory.models import Vlan
+
+
+class IDFTemplater:
+ """
+ Utility class to create a full IDF yaml file
+ """
+ net_names = ["admin", "mgmt", "private", "public"]
+ bridge_names = {
+ "admin": "br-admin",
+ "mgmt": "br-mgmt",
+ "private": "br-private",
+ "public": "br-public"
+ }
+
+ def __init__(self):
+ self.networks = {}
+ for i, name in enumerate(self.net_names):
+ self.networks[name] = {
+ "name": name,
+ "vlan": -1,
+ "interface": i,
+ "ip": "10.250." + str(i) + ".0",
+ "netmask": 24
+ }
+
+ def makeIDF(self, booking):
+ """
+ fills the installer descriptor file template with info about the resource
+ """
+ template = "dashboard/idf.yaml"
+ info = {}
+ info['version'] = "0.1"
+ info['net_config'] = self.get_net_config(booking)
+ info['fuel'] = self.get_fuel_config(booking)
+
+ return render_to_string(template, context=info)
+
+ def get_net_config(self, booking):
+ net_config = {}
+ try:
+ net_config['oob'] = self.get_oob_net(booking)
+ except Exception:
+ net_config['oob'] = {}
+ try:
+ net_config['public'] = self.get_public_net(booking)
+ except Exception:
+ net_config['public'] = {}
+
+ for net in [net for net in self.net_names if net != "public"]:
+ try:
+ net_config[net] = self.get_single_net_config(booking, net)
+ except Exception:
+ net_config[net] = {}
+
+ return net_config
+
+ def get_public_net(self, booking):
+ public = {}
+ config = booking.opnfv_config
+ public_role = config.networks.get(name="public")
+ public_vlan = Vlan.objects.filter(network=public_role.network).first()
+ public_network = PublicNetwork.objects.get(vlan=public_vlan.vlan_id, lab=booking.lab)
+ self.networks['public']['vlan'] = public_vlan.vlan_id
+ public['interface'] = self.networks['public']['interface']
+ public['vlan'] = public_network.vlan # untagged??
+ public['network'] = public_network.cidr.split("/")[0]
+ public['mask'] = public_network.cidr.split("/")[1]
+ # public['ip_range'] = 4 # necesary?
+ public['gateway'] = public_network.gateway
+ public['dns'] = ["1.1.1.1", "8.8.8.8"]
+
+ return public
+
+ def get_oob_net(self, booking):
+ net = {}
+ hosts = booking.resource.hosts.all()
+ addrs = [host.remote_management.address for host in hosts]
+ net['ip_range'] = ",".join(addrs)
+ net['vlan'] = "native"
+ return net
+
+ def get_single_net_config(self, booking, net_name):
+ config = booking.opnfv_config
+ role = config.networks.get(name=net_name)
+ vlan = Vlan.objects.filter(network=role.network).first()
+ self.networks[net_name]['vlan'] = vlan.vlan_id
+ net = {}
+ net['interface'] = self.networks[net_name]['interface']
+ net['vlan'] = vlan.vlan_id
+ net['network'] = self.networks[net_name]['ip']
+ net['mask'] = self.networks[net_name]['netmask']
+
+ return net
+
+ def get_fuel_config(self, booking):
+ fuel = {}
+ fuel['jumphost'] = {}
+ try:
+ fuel['jumphost']['bridges'] = self.get_fuel_bridges()
+ except Exception:
+ fuel['jumphost']['bridges'] = {}
+
+ fuel['network'] = {}
+ try:
+ fuel['network']['nodes'] = self.get_fuel_nodes(booking)
+ except Exception:
+ fuel['network']['nodes'] = {}
+
+ return fuel
+
+ def get_fuel_bridges(self):
+ return self.bridge_names
+
+ def get_fuel_nodes(self, booking):
+ jumphost = booking.opnfv_config.host_opnfv_config.get(
+ role__name__iexact="jumphost"
+ )
+ hosts = booking.resource.hosts.exclude(pk=jumphost.pk)
+ nodes = []
+ for host in hosts:
+ node = {}
+ ordered_interfaces = self.get_node_interfaces(host)
+ node['interfaces'] = [iface['name'] for iface in ordered_interfaces]
+ node['bus_addrs'] = [iface['bus'] for iface in ordered_interfaces]
+ nodes.append(node)
+
+ return nodes
+
+ def get_node_interfaces(self, node):
+ # TODO: this should sync with pdf ordering
+ interfaces = []
+
+ for iface in node.interfaces.all():
+ interfaces.append({"name": iface.name, "bus": iface.bus_address})
+
+ return interfaces
diff --git a/dashboard/src/resource_inventory/migrations/0009_auto_20190315_1757.py b/dashboard/src/resource_inventory/migrations/0009_auto_20190315_1757.py
new file mode 100644
index 0000000..92ed0e9
--- /dev/null
+++ b/dashboard/src/resource_inventory/migrations/0009_auto_20190315_1757.py
@@ -0,0 +1,73 @@
+# Generated by Django 2.1 on 2019-03-15 17:57
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0008_host_remote_management'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='NetworkConnection',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('vlan_is_tagged', models.BooleanField()),
+ ],
+ ),
+ migrations.CreateModel(
+ name='NetworkRole',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=100)),
+ ],
+ ),
+ migrations.RemoveField(
+ model_name='genericinterface',
+ name='vlans',
+ ),
+ migrations.RemoveField(
+ model_name='network',
+ name='vlan_id',
+ ),
+ migrations.AddField(
+ model_name='network',
+ name='bundle',
+ field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='networks', to='resource_inventory.GenericResourceBundle'),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='network',
+ name='is_public',
+ field=models.BooleanField(default=False),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='vlan',
+ name='network',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='resource_inventory.Network'),
+ ),
+ migrations.AddField(
+ model_name='networkrole',
+ name='network',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Network'),
+ ),
+ migrations.AddField(
+ model_name='networkconnection',
+ name='network',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Network'),
+ ),
+ migrations.AddField(
+ model_name='genericinterface',
+ name='connections',
+ field=models.ManyToManyField(to='resource_inventory.NetworkConnection'),
+ ),
+ migrations.AddField(
+ model_name='opnfvconfig',
+ name='networks',
+ field=models.ManyToManyField(to='resource_inventory.NetworkRole'),
+ ),
+ ]
diff --git a/dashboard/src/resource_inventory/migrations/0010_auto_20190430_1405.py b/dashboard/src/resource_inventory/migrations/0010_auto_20190430_1405.py
new file mode 100644
index 0000000..3823eaf
--- /dev/null
+++ b/dashboard/src/resource_inventory/migrations/0010_auto_20190430_1405.py
@@ -0,0 +1,54 @@
+# Generated by Django 2.1 on 2019-04-30 14:05
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0009_auto_20190315_1757'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='HostOPNFVConfig',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ],
+ ),
+ migrations.RemoveField(
+ model_name='hostconfiguration',
+ name='opnfvRole',
+ ),
+ migrations.AddField(
+ model_name='hostconfiguration',
+ name='is_head_node',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='opnfvconfig',
+ name='description',
+ field=models.CharField(blank=True, default='', max_length=600),
+ ),
+ migrations.AddField(
+ model_name='opnfvconfig',
+ name='name',
+ field=models.CharField(blank=True, default='', max_length=300),
+ ),
+ migrations.AddField(
+ model_name='hostopnfvconfig',
+ name='host_config',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='host_opnfv_config', to='resource_inventory.HostConfiguration'),
+ ),
+ migrations.AddField(
+ model_name='hostopnfvconfig',
+ name='opnfv_config',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='host_opnfv_config', to='resource_inventory.OPNFVConfig'),
+ ),
+ migrations.AddField(
+ model_name='hostopnfvconfig',
+ name='role',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='host_opnfv_configs', to='resource_inventory.OPNFVRole'),
+ ),
+ ]
diff --git a/dashboard/src/resource_inventory/models.py b/dashboard/src/resource_inventory/models.py
index bdc1f5d..b9f2c44 100644
--- a/dashboard/src/resource_inventory/models.py
+++ b/dashboard/src/resource_inventory/models.py
@@ -105,26 +105,6 @@ class RamProfile(models.Model):
return str(self.amount) + "G for " + str(self.host)
-# Networking -- located here due to import order requirements
-class Network(models.Model):
- id = models.AutoField(primary_key=True)
- vlan_id = models.IntegerField()
- name = models.CharField(max_length=100)
-
- def __str__(self):
- return self.name
-
-
-class Vlan(models.Model):
- id = models.AutoField(primary_key=True)
- vlan_id = models.IntegerField()
- tagged = models.BooleanField()
- public = models.BooleanField(default=False)
-
- def __str__(self):
- return str(self.vlan_id) + ("_T" if self.tagged else "")
-
-
# Generic resource templates
class GenericResourceBundle(models.Model):
id = models.AutoField(primary_key=True)
@@ -145,6 +125,32 @@ class GenericResourceBundle(models.Model):
return self.name
+class Network(models.Model):
+ id = models.AutoField(primary_key=True)
+ name = models.CharField(max_length=100)
+ bundle = models.ForeignKey(GenericResourceBundle, on_delete=models.CASCADE, related_name="networks")
+ is_public = models.BooleanField()
+
+ def __str__(self):
+ return self.name
+
+
+class NetworkConnection(models.Model):
+ network = models.ForeignKey(Network, on_delete=models.CASCADE)
+ vlan_is_tagged = models.BooleanField()
+
+
+class Vlan(models.Model):
+ id = models.AutoField(primary_key=True)
+ vlan_id = models.IntegerField()
+ tagged = models.BooleanField()
+ public = models.BooleanField(default=False)
+ network = models.ForeignKey(Network, on_delete=models.DO_NOTHING, null=True)
+
+ def __str__(self):
+ return str(self.vlan_id) + ("_T" if self.tagged else "")
+
+
class GenericResource(models.Model):
bundle = models.ForeignKey(GenericResourceBundle, related_name='generic_resources', on_delete=models.CASCADE)
hostname_validchars = RegexValidator(regex=r'(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))', message="Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)")
@@ -185,17 +191,14 @@ class ResourceBundle(models.Model):
return "instance of " + str(self.template)
def get_host(self, role="Jumphost"):
- return Host.objects.filter(bundle=self, config__opnfvRole__name=role).first()
-
-
-# Networking
+ return Host.objects.filter(bundle=self, config__is_head_node=True).first() # should only ever be one, but it is not an invariant in the models
class GenericInterface(models.Model):
id = models.AutoField(primary_key=True)
- vlans = models.ManyToManyField(Vlan)
profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE)
host = models.ForeignKey(GenericHost, on_delete=models.CASCADE, related_name='generic_interfaces')
+ connections = models.ManyToManyField(NetworkConnection)
def __str__(self):
return "type " + str(self.profile) + " on host " + str(self.host)
@@ -227,6 +230,11 @@ class Opsys(models.Model):
return self.name
+class NetworkRole(models.Model):
+ name = models.CharField(max_length=100)
+ network = models.ForeignKey(Network, on_delete=models.CASCADE)
+
+
class ConfigBundle(models.Model):
id = models.AutoField(primary_key=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
@@ -243,6 +251,9 @@ class OPNFVConfig(models.Model):
installer = models.ForeignKey(Installer, on_delete=models.CASCADE)
scenario = models.ForeignKey(Scenario, on_delete=models.CASCADE)
bundle = models.ForeignKey(ConfigBundle, related_name="opnfv_config", on_delete=models.CASCADE)
+ networks = models.ManyToManyField(NetworkRole)
+ name = models.CharField(max_length=300, blank=True, default="")
+ description = models.CharField(max_length=600, blank=True, default="")
def __str__(self):
return "OPNFV job with " + str(self.installer) + " and " + str(self.scenario)
@@ -288,12 +299,18 @@ class HostConfiguration(models.Model):
host = models.ForeignKey(GenericHost, related_name="configuration", on_delete=models.CASCADE)
image = models.ForeignKey(Image, on_delete=models.PROTECT)
bundle = models.ForeignKey(ConfigBundle, related_name="hostConfigurations", null=True, on_delete=models.CASCADE)
- opnfvRole = models.ForeignKey(OPNFVRole, on_delete=models.SET(get_sentinal_opnfv_role))
+ is_head_node = models.BooleanField(default=False)
def __str__(self):
return "config with " + str(self.host) + " and image " + str(self.image)
+class HostOPNFVConfig(models.Model):
+ role = models.ForeignKey(OPNFVRole, related_name="host_opnfv_configs", on_delete=models.CASCADE)
+ host_config = models.ForeignKey(HostConfiguration, related_name="host_opnfv_config", on_delete=models.CASCADE)
+ opnfv_config = models.ForeignKey(OPNFVConfig, related_name="host_opnfv_config", on_delete=models.CASCADE)
+
+
class RemoteInfo(models.Model):
address = models.CharField(max_length=15)
mac_address = models.CharField(max_length=17)
@@ -344,3 +361,11 @@ class Interface(models.Model):
def __str__(self):
return self.mac_address + " on host " + str(self.host)
+
+
+class OPNFV_SETTINGS():
+ """
+ This is a static configuration class
+ """
+ # all the required network types in PDF/IDF spec
+ NETWORK_ROLES = ["public", "private", "admin", "mgmt"]
diff --git a/dashboard/src/resource_inventory/pdf_templater.py b/dashboard/src/resource_inventory/pdf_templater.py
index a50f04c..2302530 100644
--- a/dashboard/src/resource_inventory/pdf_templater.py
+++ b/dashboard/src/resource_inventory/pdf_templater.py
@@ -19,15 +19,15 @@ class PDFTemplater:
"""
@classmethod
- def makePDF(cls, resource):
+ def makePDF(cls, booking):
"""
fills the pod descriptor file template with info about the resource
"""
template = "dashboard/pdf.yaml"
info = {}
- info['details'] = cls.get_pdf_details(resource)
- info['jumphost'] = cls.get_pdf_jumphost(resource)
- info['nodes'] = cls.get_pdf_nodes(resource)
+ info['details'] = cls.get_pdf_details(booking.resource)
+ info['jumphost'] = cls.get_pdf_jumphost(booking)
+ info['nodes'] = cls.get_pdf_nodes(booking)
return render_to_string(template, context=info)
@@ -63,26 +63,40 @@ class PDFTemplater:
return details
@classmethod
- def get_pdf_jumphost(cls, resource):
+ def get_jumphost(cls, booking):
+ jumphost = None
+ if booking.opnfv_config:
+ jumphost_opnfv_config = booking.opnfv_config.host_opnfv_config.get(
+ role__name__iexact="jumphost"
+ )
+ jumphost = booking.resource.hosts.get(config=jumphost_opnfv_config.host_config)
+ else: # if there is no opnfv config, use headnode
+ jumphost = Host.objects.filter(
+ bundle=booking.resource,
+ config__is_head_node=True
+ ).first()
+
+ return jumphost
+
+ @classmethod
+ def get_pdf_jumphost(cls, booking):
"""
returns a dict of all the info for the "jumphost" section
"""
- jumphost = Host.objects.get(bundle=resource, config__opnfvRole__name__iexact="jumphost")
+ jumphost = cls.get_jumphost(booking)
jumphost_info = cls.get_pdf_host(jumphost)
- remote_params = jumphost_info['remote_management'] # jumphost has extra block not in normal hosts
- remote_params.pop("address")
- remote_params.pop("mac_address")
- jumphost_info['remote_params'] = remote_params
jumphost_info['os'] = jumphost.config.image.os.name
return jumphost_info
@classmethod
- def get_pdf_nodes(cls, resource):
+ def get_pdf_nodes(cls, booking):
"""
returns a list of all the "nodes" (every host except jumphost)
"""
pdf_nodes = []
- nodes = Host.objects.filter(bundle=resource).exclude(config__opnfvRole__name__iexact="jumphost")
+ nodes = set(Host.objects.filter(bundle=booking.resource))
+ nodes.discard(cls.get_jumphost(booking))
+
for node in nodes:
pdf_nodes.append(cls.get_pdf_host(node))
@@ -105,7 +119,7 @@ class PDFTemplater:
for interface in host.interfaces.all():
host_info['interfaces'].append(cls.get_pdf_host_iface(interface))
- host_info['remote_management'] = cls.get_pdf_host_remote_management(host)
+ host_info['remote'] = cls.get_pdf_host_remote_management(host)
return host_info
@@ -168,11 +182,12 @@ class PDFTemplater:
"""
gives the remote params of the host
"""
+ man = host.remote_management
mgmt = {}
- mgmt['address'] = "I dunno"
- mgmt['mac_address'] = "I dunno"
- mgmt['pass'] = "I dunno"
- mgmt['type'] = "I dunno"
- mgmt['user'] = "I dunno"
- mgmt['versions'] = ["I dunno"]
+ mgmt['address'] = man.address
+ mgmt['mac_address'] = man.mac_address
+ mgmt['pass'] = man.password
+ mgmt['type'] = man.management_type
+ mgmt['user'] = man.user
+ mgmt['versions'] = [man.versions]
return mgmt
diff --git a/dashboard/src/resource_inventory/resource_manager.py b/dashboard/src/resource_inventory/resource_manager.py
index 52b0055..652e4e3 100644
--- a/dashboard/src/resource_inventory/resource_manager.py
+++ b/dashboard/src/resource_inventory/resource_manager.py
@@ -14,7 +14,14 @@ from dashboard.exceptions import (
ResourceProvisioningException,
ModelValidationException,
)
-from resource_inventory.models import Host, HostConfiguration, ResourceBundle, HostProfile
+from resource_inventory.models import (
+ Host,
+ HostConfiguration,
+ ResourceBundle,
+ HostProfile,
+ Network,
+ Vlan
+)
class ResourceManager:
@@ -66,55 +73,71 @@ class ResourceManager:
self.releaseHost(host)
resourceBundle.delete()
- def convertResourceBundle(self, genericResourceBundle, lab=None, config=None):
+ def get_vlans(self, genericResourceBundle):
+ networks = {}
+ vlan_manager = genericResourceBundle.lab.vlan_manager
+ for network in genericResourceBundle.networks.all():
+ if network.is_public:
+ public_net = vlan_manager.get_public_vlan()
+ vlan_manager.reserve_public_vlan(public_net.vlan)
+ networks[network.name] = public_net.vlan
+ else:
+ vlan = vlan_manager.get_vlan()
+ vlan_manager.reserve_vlans(vlan)
+ networks[network.name] = vlan
+ return networks
+
+ def convertResourceBundle(self, genericResourceBundle, config=None):
"""
Takes in a GenericResourceBundle and 'converts' it into a ResourceBundle
"""
- resource_bundle = ResourceBundle()
- resource_bundle.template = genericResourceBundle
- resource_bundle.save()
-
- hosts = genericResourceBundle.getHosts()
-
- # current supported case: user creating new booking
- # currently unsupported: editing existing booking
-
+ resource_bundle = ResourceBundle.objects.create(template=genericResourceBundle)
+ generic_hosts = genericResourceBundle.getHosts()
physical_hosts = []
- for host in hosts:
+ vlan_map = self.get_vlans(genericResourceBundle)
+
+ for generic_host in generic_hosts:
host_config = None
if config:
- host_config = HostConfiguration.objects.get(bundle=config, host=host)
+ host_config = HostConfiguration.objects.get(bundle=config, host=generic_host)
try:
- physical_host = self.acquireHost(host, genericResourceBundle.lab.name)
+ physical_host = self.acquireHost(generic_host, genericResourceBundle.lab.name)
except ResourceAvailabilityException:
- self.fail_acquire(physical_hosts)
+ self.fail_acquire(physical_hosts, vlan_map, genericResourceBundle)
raise ResourceAvailabilityException("Could not provision hosts, not enough available")
try:
physical_host.bundle = resource_bundle
- physical_host.template = host
+ physical_host.template = generic_host
physical_host.config = host_config
physical_hosts.append(physical_host)
- self.configureNetworking(physical_host)
+ self.configureNetworking(physical_host, vlan_map)
except Exception:
- self.fail_acquire(physical_hosts)
+ self.fail_acquire(physical_hosts, vlan_map, genericResourceBundle)
raise ResourceProvisioningException("Network configuration failed.")
try:
physical_host.save()
except Exception:
- self.fail_acquire(physical_hosts)
+ self.fail_acquire(physical_hosts, vlan_map, genericResourceBundle)
raise ModelValidationException("Saving hosts failed")
return resource_bundle
- def configureNetworking(self, host):
+ def configureNetworking(self, host, vlan_map):
generic_interfaces = list(host.template.generic_interfaces.all())
for int_num, physical_interface in enumerate(host.interfaces.all()):
generic_interface = generic_interfaces[int_num]
physical_interface.config.clear()
- for vlan in generic_interface.vlans.all():
- physical_interface.config.add(vlan)
+ for connection in generic_interface.connections.all():
+ physical_interface.config.add(
+ Vlan.objects.create(
+ vlan_id=vlan_map[connection.network.name],
+ tagged=connection.vlan_is_tagged,
+ public=connection.network.is_public,
+ network=connection.network
+ )
+ )
# private interface
def acquireHost(self, genericHost, labName):
@@ -136,6 +159,16 @@ class ResourceManager:
host.booked = False
host.save()
- def fail_acquire(self, hosts):
+ def releaseNetworks(self, grb, vlan_manager, vlans):
+ for net_name, vlan_id in vlans.items():
+ net = Network.objects.get(name=net_name, bundle=grb)
+ if(net.is_public):
+ vlan_manager.release_public_vlan(vlan_id)
+ else:
+ vlan_manager.release_vlans(vlan_id)
+
+ def fail_acquire(self, hosts, vlans, grb):
+ vlan_manager = grb.lab.vlan_manager
+ self.releaseNetworks(grb, vlan_manager, vlans)
for host in hosts:
self.releaseHost(host)
diff --git a/dashboard/src/resource_inventory/urls.py b/dashboard/src/resource_inventory/urls.py
index 4e159ba..a72871b 100644
--- a/dashboard/src/resource_inventory/urls.py
+++ b/dashboard/src/resource_inventory/urls.py
@@ -25,10 +25,11 @@ Including another URLconf
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url
-from resource_inventory.views import HostView
+from resource_inventory.views import HostView, hostprofile_detail_view
app_name = "resource"
urlpatterns = [
- url(r'^hosts$', HostView.as_view(), name='hosts')
+ url(r'^hosts$', HostView.as_view(), name='hosts'),
+ url(r'^profiles/(?P<hostprofile_id>.+)/$', hostprofile_detail_view, name='host_detail'),
]
diff --git a/dashboard/src/resource_inventory/views.py b/dashboard/src/resource_inventory/views.py
index 2937bd7..8c3d899 100644
--- a/dashboard/src/resource_inventory/views.py
+++ b/dashboard/src/resource_inventory/views.py
@@ -9,8 +9,10 @@
from django.views.generic import TemplateView
+from django.shortcuts import get_object_or_404
+from django.shortcuts import render
-from resource_inventory.models import Host
+from resource_inventory.models import HostProfile, Host
class HostView(TemplateView):
@@ -21,3 +23,16 @@ class HostView(TemplateView):
hosts = Host.objects.filter(working=True)
context.update({'hosts': hosts, 'title': "Hardware Resources"})
return context
+
+
+def hostprofile_detail_view(request, hostprofile_id):
+ hostprofile = get_object_or_404(HostProfile, id=hostprofile_id)
+
+ return render(
+ request,
+ "resource/hostprofile_detail.html",
+ {
+ 'title': "Host Type: " + str(hostprofile.name),
+ 'hostprofile': hostprofile
+ }
+ )
diff --git a/dashboard/src/templates/base.html b/dashboard/src/templates/base.html
index 02c67dc..f48a201 100644
--- a/dashboard/src/templates/base.html
+++ b/dashboard/src/templates/base.html
@@ -134,6 +134,7 @@
<button class="btn drop_btn" onclick="cwf(1)">Design a Pod</button>
<button class="btn drop_btn" onclick="cwf(2)">Configure a Pod</button>
<button class="btn drop_btn" onclick="cwf(3)">Create a Snapshot</button>
+ <button class="btn drop_btn" onclick="cwf(4)">Configure OPNFV</button>
</div>
</li>
<li>
diff --git a/dashboard/src/templates/booking/booking_table.html b/dashboard/src/templates/booking/booking_table.html
index e0c5f49..32a0146 100644
--- a/dashboard/src/templates/booking/booking_table.html
+++ b/dashboard/src/templates/booking/booking_table.html
@@ -30,7 +30,7 @@
{{ booking.end }}
</td>
<td>
- {{ booking.resource.get_host.config.image.os.name }}
+ {{ booking.resource.get_head_node.config.image.os.name }}
</td>
</tr>
{% endfor %}
diff --git a/dashboard/src/templates/booking/quick_deploy.html b/dashboard/src/templates/booking/quick_deploy.html
index 819bf05..38294b2 100644
--- a/dashboard/src/templates/booking/quick_deploy.html
+++ b/dashboard/src/templates/booking/quick_deploy.html
@@ -26,6 +26,14 @@
.grid_element_2third {
grid-column-start: span 8;
}
+ #id_length {
+ -moz-appearance: none;
+ border: none;
+ box-shadow: none;
+ }
+ input[type=range]::-moz-range-track {
+ background: #cccccc;
+ }
</style>
{% bootstrap_form_errors form type='non_fields' %}
<form id="quick_booking_form" action="/booking/quick/" method="POST" class="form">
@@ -108,6 +116,12 @@
$('#id_image').children().hide();
+ for( var i = 0; i < drop.childNodes.length; i++ )
+ {
+ drop.childNodes[i].disabled = true; // closest we can get on safari to hiding it outright
+ }
+
+
var empty_map = {}
for ( var i=0; i < drop.childNodes.length; i++ )
@@ -130,12 +144,13 @@
if( image_object.host_profile == host_pk && image_object.lab == lab_pk )
{
drop.childNodes[i].style.display = "inherit";
+ drop.childNodes[i].disabled = false;
}
}
}
}
- $('#id_image').children().hide();
+ imageHider();
$('#id_installer').children().hide();
$('#id_scenario').children().hide();
@@ -165,7 +180,13 @@
}
targ_id = "#" + target;
+
$(targ_id).children().hide();
+
+ for (var i = 0; i < document.getElementById(target).childNodes.length; i++)
+ {
+ document.getElementById(target).childNodes[i].disabled = true;
+ }
var drop = document.getElementById(master);
var opts = target_filter[drop.options[drop.selectedIndex].value];
if (!opts) {
@@ -182,6 +203,7 @@
for (var i = 0; i < document.getElementById(target).childNodes.length; i++) {
if (document.getElementById(target).childNodes[i].value in opts && !(document.getElementById(target).childNodes[i].value in emptyMap) ) {
document.getElementById(target).childNodes[i].style.display = "inherit";
+ document.getElementById(target).childNodes[i].disabled = false;
}
}
}
diff --git a/dashboard/src/templates/booking/steps/booking_meta.html b/dashboard/src/templates/booking/steps/booking_meta.html
index e4881ae..fe43f53 100644
--- a/dashboard/src/templates/booking/steps/booking_meta.html
+++ b/dashboard/src/templates/booking/steps/booking_meta.html
@@ -21,6 +21,15 @@
grid-template-columns: 45% 10% 45%;
border: none;
}
+
+ #id_length {
+ -moz-appearance: none;
+ border: none;
+ box-shadow: none;
+ }
+ input[type=range]::-moz-range-track {
+ background: #cccccc;
+ }
</style>
{% bootstrap_form_errors form type='non_fields' %}
diff --git a/dashboard/src/templates/config_bundle/steps/assign_host_roles.html b/dashboard/src/templates/config_bundle/steps/assign_host_roles.html
new file mode 100644
index 0000000..3ba7665
--- /dev/null
+++ b/dashboard/src/templates/config_bundle/steps/assign_host_roles.html
@@ -0,0 +1,22 @@
+{% extends "config_bundle/steps/table_formset.html" %}
+
+{% load bootstrap3 %}
+
+{% block table %}
+<thead>
+ <tr>
+ <th>Host</th>
+ <th>Role</th>
+ </tr>
+</thead>
+<tbody>
+ {% for form in formset %}
+ <tr>
+ <td>{% bootstrap_field form.host_name show_label=False %}</td>
+ <td>{% bootstrap_field form.role show_label=False %}</td>
+ </tr>
+ {% endfor %}
+</tbody>
+
+{{formset.management_form}}
+{% endblock table %}
diff --git a/dashboard/src/templates/config_bundle/steps/assign_network_roles.html b/dashboard/src/templates/config_bundle/steps/assign_network_roles.html
new file mode 100644
index 0000000..0e887d6
--- /dev/null
+++ b/dashboard/src/templates/config_bundle/steps/assign_network_roles.html
@@ -0,0 +1,22 @@
+{% extends "config_bundle/steps/table_formset.html" %}
+
+{% load bootstrap3 %}
+
+{% block table %}
+<thead>
+ <tr>
+ <th>Role</th>
+ <th>Network</th>
+ </tr>
+</thead>
+<tbody>
+ {% for form in formset %}
+ <tr>
+ <td>{% bootstrap_field form.role show_label=False %}</td>
+ <td>{% bootstrap_field form.network show_label=False %}</td>
+ </tr>
+ {% endfor %}
+</tbody>
+
+{{formset.management_form}}
+{% endblock table %}
diff --git a/dashboard/src/templates/config_bundle/steps/config_software.html b/dashboard/src/templates/config_bundle/steps/config_software.html
index e1f9541..b181c7e 100644
--- a/dashboard/src/templates/config_bundle/steps/config_software.html
+++ b/dashboard/src/templates/config_bundle/steps/config_software.html
@@ -8,58 +8,12 @@
<form action="/wf/workflow/" method="POST" id="software_config_form" class="form">
{% csrf_token %}
<p>Give it a name:</p>
- {{ form.name }}
+ {% bootstrap_field form.name %}
<p>And a description:</p>
- {{ form.description }}
- <div id="hidden" style="display:none;">
- <p>Install OPNFV?</p>
- {{ form.opnfv }}
- <p>Choose your:</p>
- <table>
- <thead>
- <tr>
- <th>Installer</th>
- <th>Scenario</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>{{form.installer}}</td>
- <td>{{form.scenario}}</td>
- </tr>
- </tbody>
- </table>
- </div>
-
+ {% bootstrap_field form.description %}
</form>
-<script>
-var supported = {{supported|safe}};
-var installer_drop = document.getElementById("id_installer");
-installer_drop.addEventListener("change", filter);
-var scenario_drop = document.getElementById("id_scenario");
-var scenario_options = {};
-for(var i=0; i<scenario_drop.options.length; i++){
- var option = scenario_drop.options[i];
- scenario_options[option.text] = option;
-}
-
-scenario_drop.disabled=true;
-
-function filter(){
- //clear out existing options
- while(scenario_drop.firstChild){
- scenario_drop.removeChild(scenario_drop.firstChild)
- }
- var installer = installer_drop.options[installer_drop.selectedIndex].text;
- var options = supported[installer];
- for(var i=0; i<options.length; i++){
- scenario_drop.appendChild(scenario_options[options[i]]);
- }
- scenario_drop.disabled = false;
-}
-</script>
{% endblock content %}
diff --git a/dashboard/src/templates/config_bundle/steps/define_software.html b/dashboard/src/templates/config_bundle/steps/define_software.html
index 8e7be91..ba1ff34 100644
--- a/dashboard/src/templates/config_bundle/steps/define_software.html
+++ b/dashboard/src/templates/config_bundle/steps/define_software.html
@@ -1,102 +1,55 @@
-{% extends "workflow/viewport-element.html" %}
-{% load staticfiles %}
+{% extends "config_bundle/steps/table_formset.html" %}
{% load bootstrap3 %}
+{% block table %}
+ <thead>
+ <tr>
+ <th>Device</th>
+ <th>Image</th>
+ <th>HeadNode</th>
+ </tr>
+ </thead>
+ <tbody>
+{% for form in formset %}
+ <tr>
+ <td>{% bootstrap_field form.host_name show_label=False %}</td>
+ <td>{% bootstrap_field form.image show_label=False %}</td>
+ <td class="table_hidden_input_parent">
+ <input id="radio_{{forloop.counter}}" class="my_radio" type="radio" name="headnode" value="{{forloop.counter}}">
+ {{ form.headnode }}
+ </td>
+ </tr>
+{% endfor %}
+{{formset.management_form}}
+
+{% endblock table %}
+
+{% block tablejs %}
+<script>
+
+ document.getElementById("radio_{{headnode}}").checked = true;
+
+</script>
+{% endblock tablejs %}
-{% block extrahead %}
- <!-- DataTables CSS -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
- rel="stylesheet">
- <!-- DataTables Responsive CSS -->
- <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}" rel="stylesheet">
-{% endblock extrahead %}
-
-{% block content %}
-{% if error %}
- <h1 style="text-align:center;">{{ error }}</h1>
-{% else %}
- <form style="width: 90%; margin: 5%;" method="post" action="" class="form" id="softwaredefinitionform">
- {% csrf_token %}
-
- <div class="row">
- <div class="col-lg-12">
- <div class="dataTables_wrapper">
- <table class="table table-striped table-bordered table-hover" id="table" cellspacing="0"
- width="100%">
-
- {% block table %}
- <thead>
- <tr>
- <th>Device</th>
- <th>Role</th>
- <th>Image</th>
- </tr>
- </thead>
- <tbody>
- {% for form in formset %}
- <tr>
- {% for field in form %}
- <td>{{ field }}</td>
- {% endfor %}
- </tr>
- {% endfor %}
- {{formset.management_form}}
-
- {% endblock table %}
-
- </table>
- </div>
- <!-- /.table-responsive -->
- <!-- /.panel-body -->
- <!-- /.panel -->
- </div>
- <!-- /.col-lg-12 -->
- </div>
- </form>
-
- <script>
-function filter_images(){
- var filter_data = {{filter_data|safe}};
- for(var key in filter_data){
- var dropdown = document.getElementById(key);
- var to_remove = filter_data[key];
- for(var i=0; i<to_remove.length; i++){
- for(var j=dropdown.children.length-1; j>=0; j--){
- if(dropdown.children[j].text == to_remove[i]){
- dropdown.removeChild(dropdown.children[j]);
- }
- }
- }
+{% block onleave %}
+var parents = document.getElementsByClassName("table_hidden_input_parent");
+for(var i=0; i<parents.length; i++){
+ var node = parents[i];
+ var radio = node.getElementsByClassName("my_radio")[0];
+ var checkbox = radio.nextElementSibling;
+ if(radio.checked){
+ checkbox.value = "True";
}
}
-filter_images();
- </script>
-{% endif %}
-{% endblock content %}
-
-{% block extrajs %}
- {{ block.super }}
- <!-- DataTables JavaScript -->
-
- <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
- <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
-
- <script src={% static "js/dataTables-sort.js" %}></script>
-
- {% block tablejs %}
- {% endblock tablejs %}
-{% endblock extrajs %}
-
-
-{% block onleave %}
-var form = $("#softwaredefinitionform");
+var form = $("#table_formset");
var formData = form.serialize();
var req = new XMLHttpRequest();
req.open("POST", "/wf/workflow/", false);
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
req.onerror = function() { alert("problem with form submission"); }
req.send(formData);
-{% endblock %}
+{% endblock onleave %}
diff --git a/dashboard/src/templates/config_bundle/steps/pick_installer.html b/dashboard/src/templates/config_bundle/steps/pick_installer.html
new file mode 100644
index 0000000..3b170d9
--- /dev/null
+++ b/dashboard/src/templates/config_bundle/steps/pick_installer.html
@@ -0,0 +1,32 @@
+{% extends "workflow/viewport-element.html" %}
+{% load staticfiles %}
+
+{% load bootstrap3 %}
+
+{% block content %}
+
+{% if unavailable %}
+<h1>Please choose a config bundle first</h1>
+{% else %}
+
+<form id="installer_form" action="/wf/workflow/" method="POST" id="installer_config_form" class="form">
+ {% csrf_token %}
+ <p>Choose your installer:</p>
+ {% bootstrap_field form.installer %}
+ <p>Choose your scenario:</p>
+ {% bootstrap_field form.scenario %}
+</form>
+
+{% endif %}
+
+{% endblock content %}
+
+{% block onleave %}
+var form = $("#installer_form");
+var formData = form.serialize();
+var req = new XMLHttpRequest();
+req.open("POST", "/wf/workflow/", false);
+req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+req.onerror = function() { alert("problem with form submission"); }
+req.send(formData);
+{% endblock %}
diff --git a/dashboard/src/templates/config_bundle/steps/table_formset.html b/dashboard/src/templates/config_bundle/steps/table_formset.html
new file mode 100644
index 0000000..ad2c5a3
--- /dev/null
+++ b/dashboard/src/templates/config_bundle/steps/table_formset.html
@@ -0,0 +1,63 @@
+{% extends "workflow/viewport-element.html" %}
+{% load staticfiles %}
+
+{% load bootstrap3 %}
+
+{% block extrahead %}
+ <!-- DataTables CSS -->
+ <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ rel="stylesheet">
+
+ <!-- DataTables Responsive CSS -->
+ <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}" rel="stylesheet">
+{% endblock extrahead %}
+
+{% block content %}
+{% if error %}
+ <h1 style="text-align:center;">{{ error }}</h1>
+{% else %}
+<div style="padding: 5%;">
+ <form method="post" action="" class="form" id="table_formset">
+ {% csrf_token %}
+
+ <div class="row">
+ <div class="col-lg-12">
+ <div class="dataTables_wrapper">
+ <table class="table table-striped table-bordered table-hover" id="table" cellspacing="0" width="100%">
+
+ {% block table %}
+ {% endblock table %}
+
+ </table>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
+
+{% endif %}
+{% endblock content %}
+
+{% block extrajs %}
+ {{ block.super }}
+ <!-- DataTables JavaScript -->
+
+ <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
+
+ <script src={% static "js/dataTables-sort.js" %}></script>
+
+ {% block tablejs %}
+ {% endblock tablejs %}
+{% endblock extrajs %}
+
+
+{% block onleave %}
+var form = $("#table_formset");
+var formData = form.serialize();
+var req = new XMLHttpRequest();
+req.open("POST", "/wf/workflow/", false);
+req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+req.onerror = function() { alert("problem with form submission"); }
+req.send(formData);
+{% endblock %}
diff --git a/dashboard/src/templates/dashboard/idf.yaml b/dashboard/src/templates/dashboard/idf.yaml
index 5da20c4..9e0cc26 100644
--- a/dashboard/src/templates/dashboard/idf.yaml
+++ b/dashboard/src/templates/dashboard/idf.yaml
@@ -1,8 +1,9 @@
+---
idf:
version: {{version|default:"0.1"}}
net_config:
oob:
- ip-range: {{net_config.oob.ip-range}}
+ ip-range: {{net_config.oob.ip_range}}
vlan: {{net_config.oob.vlan}}
admin:
interface: {{net_config.admin.interface}}
@@ -24,13 +25,11 @@ idf:
vlan: {{net_config.public.vlan}}
network: {{net_config.public.network}}
mask: {{net_config.public.mask}}
- ip-range: {{net_config.public.ip-range}}
+ ip-range: {{net_config.public.ip_range}}
mask: {{net_config.public.mask}}
gateway: {{net_config.public.gateway}}
- dns:
- {% for serv in net_config.public.dns %}
- - {{serv}}
- {% endfor %}
+ dns: {% for serv in net_config.public.dns %}
+ - {{serv}}{% endfor %}
fuel:
jumphost:
bridges:
@@ -38,15 +37,10 @@ idf:
mgmt: {{fuel.jumphost.bridges.mgmt}}
private: {{fuel.jumphost.bridges.private}}
public: {{fuel.jumphost.bridges.public}}
- network:
- {% for node in fuel.network.nodes %}
+ network: {% for node in fuel.network.nodes %}
node:
- - interfaces:
- {% for iface in node.interfaces %}
- - {{ iface }}
- {% endfor %}
- - busaddr:
- {% for addr in node.bus_addrs %}
- - {{addr}}
- {% endfor %}
+ - interfaces: {% for iface in node.interfaces %}
+ - {{ iface }}{% endfor %}
+ - busaddr: {% for addr in node.bus_addrs %}
+ - {{addr}}{% endfor %}
{% endfor %}
diff --git a/dashboard/src/templates/dashboard/lab_detail.html b/dashboard/src/templates/dashboard/lab_detail.html
index a30ac9e..7938e86 100644
--- a/dashboard/src/templates/dashboard/lab_detail.html
+++ b/dashboard/src/templates/dashboard/lab_detail.html
@@ -62,6 +62,7 @@
<tr>
<td>{{profile.name}}</td>
<td>{{profile.description}}</td>
+ <td><a href="/resource/profiles/{{ profile.id }}" class="btn btn-primary">Profile</a></td>
</tr>
{% endfor %}
</table>
diff --git a/dashboard/src/templates/resource/hostprofile_detail.html b/dashboard/src/templates/resource/hostprofile_detail.html
new file mode 100644
index 0000000..0776b9e
--- /dev/null
+++ b/dashboard/src/templates/resource/hostprofile_detail.html
@@ -0,0 +1,116 @@
+{% extends "base.html" %}
+{% load staticfiles %}
+
+{% block content %}
+<div class="row">
+ <div class="col-lg-6">
+ <div class="panel panel-default">
+ <div class="panel-heading clearfix">
+ <h4 style="display: inline;">Available at</h4>
+ <a data-toggle="collapse" data-target="#panel_overview" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ </div>
+ <div class="panel-body" id="panel_overview">
+ <table class="table">
+ <tr>
+ <td>
+ <ul>
+ {% for lab in hostprofile.labs.all %}
+ <li>{{lab.name}}</li>
+ {% endfor %}
+ </ul>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ <div class="panel panel-default">
+ <div class="panel-heading clearfix">
+ <h4 style="display: inline;">RAM</h4>
+ <a data-toggle="collapse" data-target="#panel_overview" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ </div>
+ <div class="panel-body" id="panel_overview">
+ <table class="table">
+ <tr>
+ <td>{{hostprofile.ramprofile.first.amount}}G,
+ {{hostprofile.ramprofile.first.channels}} channels</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ <div class="panel panel-default">
+ <div class="panel-heading clearfix">
+ <h4 style="display: inline;">CPU</h4>
+ <a data-toggle="collapse" data-target="#panel_overview" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ </div>
+ <div class="panel-body" id="panel_overview">
+ <table class="table">
+ <tr>
+ <td>Arch:</td>
+ <td>{{hostprofile.cpuprofile.first.architecture}}</td>
+ </tr>
+ <tr>
+ <td>Cores:</td>
+ <td>{{hostprofile.cpuprofile.first.cores}}</td>
+ </tr>
+ <tr>
+ <td>Sockets:</td>
+ <td>{{hostprofile.cpuprofile.first.cpus}}</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-6">
+ <div class="panel panel-default">
+ <div class="panel-heading clearfix">
+ <h4 style="display: inline;">Interfaces</h4>
+ <a data-toggle="collapse" data-target="#panel_overview" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ </div>
+ <div class="panel-body" id="panel_overview">
+ <table class="table">
+ {% for intprof in hostprofile.interfaceprofile.all %}
+ <tr>
+ <td>
+ <table class="table borderless">
+ <tr>
+ <td>Name:</td>
+ <td>{{intprof.name}}</td>
+ </tr>
+ <tr>
+ <td>Speed:</td>
+ <td>{{intprof.speed}}</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-6">
+ <div class="panel panel-default">
+ <div class="panel-heading clearfix">
+ <h4 style="display: inline;">Disk</h4>
+ <a data-toggle="collapse" data-target="#panel_overview" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ </div>
+ <div class="panel-body" id="panel_overview">
+ <table class="table">
+ <tr>
+ <td>Size:</td>
+ <td>{{hostprofile.storageprofile.first.size}} GiB</td>
+ </tr>
+ <tr>
+ <td>Type:</td>
+ <td>{{hostprofile.storageprofile.first.media_type}}</td>
+ </tr>
+ <tr>
+ <td>Mount Point:</td>
+ <td>{{hostprofile.storageprofile.first.name}}</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
+</div>
+{% endblock content %}
diff --git a/dashboard/src/templates/resource/hosts.html b/dashboard/src/templates/resource/hosts.html
index 4bf64e0..69b7231 100644
--- a/dashboard/src/templates/resource/hosts.html
+++ b/dashboard/src/templates/resource/hosts.html
@@ -17,7 +17,7 @@
{{ host.name }}
</td>
<td>
- {{ host.profile }}
+ <a href="profiles/{{ host.profile.id }}">{{ host.profile }}</a>
</td>
<td>
{{ host.booked }}
diff --git a/dashboard/src/templates/resource/steps/meta_info.html b/dashboard/src/templates/resource/steps/meta_info.html
index 7a1b56a..da98267 100644
--- a/dashboard/src/templates/resource/steps/meta_info.html
+++ b/dashboard/src/templates/resource/steps/meta_info.html
@@ -7,7 +7,7 @@
<style>
#resource_meta_form {
- margin: 80px;
+ padding: 80px;
display: grid;
}
diff --git a/dashboard/src/templates/resource/steps/pod_definition.html b/dashboard/src/templates/resource/steps/pod_definition.html
index 8599bb0..2cb6257 100644
--- a/dashboard/src/templates/resource/steps/pod_definition.html
+++ b/dashboard/src/templates/resource/steps/pod_definition.html
@@ -22,23 +22,9 @@ var netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC
var hostCount = 0;
var lastHostBottom = 100;
var networks = new Set([]);
-var network_names = new Set([]);
var has_public_net = false;
-var vlans = {{vlans|default:'null'}};
-var vlan_string = "";
function main(graphContainer, overviewContainer, toolbarContainer) {
- if(vlans){
- for(var i=0; i<vlans.length-1; i++){
- vlan_string += vlans[i] + ", ";
- }
- if(vlans.length > 0){
- vlan_string += vlans[vlans.length-1];
- }
-
- var str = "Available vlans for your POD: " + vlan_string;
- document.getElementById("vlan_notice").innerHTML = str;
- }
//check if the browser is supported
if (!mxClient.isBrowserSupported()) {
mxUtils.error('Browser is not supported', 200, false);
@@ -55,14 +41,6 @@ function main(graphContainer, overviewContainer, toolbarContainer) {
var model = graph.getModel();
editor.setGraphContainer(graphContainer);
- {% if debug %}
- editor.addAction('printXML', function(editor, cell) {
- mxLog.write(encodeGraph(graph));
- mxLog.show();
- });
- {% endif %}
-
-
doGlobalConfig(graph);
currentGraph = graph;
@@ -70,7 +48,6 @@ function main(graphContainer, overviewContainer, toolbarContainer) {
restoreFromXml('{{xml|safe}}', editor);
{% elif hosts %}
{% for host in hosts %}
-
var host = {{host|safe}};
makeHost(host);
{% endfor %}
@@ -87,12 +64,15 @@ function main(graphContainer, overviewContainer, toolbarContainer) {
addToolbarButton(editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true);
{% if debug %}
+ editor.addAction('printXML', function(editor, cell) {
+ mxLog.write(encodeGraph(graph));
+ mxLog.show();
+ });
addToolbarButton(editor, toolbarContainer, 'printXML', '', '/static/img/mxgraph/fit_to_size.png', true);
{% endif %}
var outline = new mxOutline(graph, overviewContainer);
-
var checkAllowed = function(edge, terminal, source) {
//check if other terminal is null, and that they are different
otherTerminal = edge.getTerminal(!source);
@@ -145,14 +125,14 @@ function main(graphContainer, overviewContainer, toolbarContainer) {
}
});
- createDeleteDialog = function(id)
- {
- var content = document.createElement('div');
- var innerHTML = "<button style='width: 46%;' onclick=deleteCell('" + id + "');>Remove</button>"
- innerHTML += "<button style='width: 46%;' onclick='currentWindow.destroy();'>Cancel</button>"
- content.innerHTML = innerHTML;
- showWindow(currentGraph, 'Do you want to delete this network?', content, 200, 62);
+ createDeleteDialog = function(id) {
+ var content = document.createElement('div');
+ var innerHTML = "<button style='width: 46%;' onclick=deleteCell('" + id + "');>Remove</button>"
+ innerHTML += "<button style='width: 46%;' onclick='currentWindow.destroy();'>Cancel</button>"
+ content.innerHTML = innerHTML;
+ showWindow(currentGraph, 'Do you want to delete this network?', content, 200, 62);
}
+
graph.dblClick = function(evt, cell) {
if( cell != null ){
@@ -167,8 +147,6 @@ function main(graphContainer, overviewContainer, toolbarContainer) {
}
}
};
- graph.setCellsSelectable(false);
- graph.setCellsMovable(false);
updateHosts({{ removed_hosts|default:"[]"|safe }});
if(!has_public_net){
@@ -197,10 +175,8 @@ function restoreFromXml(xml, editor) {
var cell = root.getChildAt(i);
if(cell.getId().indexOf("network") > -1) {
var info = JSON.parse(cell.getValue());
- var vlan_id = info['vlan_id'];
- networks.add(vlan_id);
var name = info['name'];
- network_names.add(name);
+ networks.add(name);
var styles = cell.getStyle().split(";");
var color = null;
for(var j=0; j< styles.length; j++){
@@ -211,11 +187,10 @@ function restoreFromXml(xml, editor) {
}
}
if(info.public){
- vlan_id = "";
has_public_net = true;
}
netCount++;
- makeSidebarNetwork(name, vlan_id, color, cell.getId());
+ makeSidebarNetwork(name, color, cell.getId());
}
}
}
@@ -228,50 +203,27 @@ function deleteCell(cellId) {
}
currentGraph.removeCells([cell]);
currentWindow.destroy();
-
}
function newNetworkWindow() {
var innerHtml = 'Name: <input type="text" name="net_name" id="net_name_input" style="margin:5px;"><br>';
- innerHtml += 'Vlan: <input type="number" step="1" name="vlan_id" id="vlan_id_input" style="margin:5px;"><br>';
innerHtml += '<button style="width: 46%;" onclick="parseNetworkWindow()">Okay</button>';
innerHtml += '<button style="width: 46%;" onclick="currentWindow.destroy();">Cancel</button><br>';
- innerHtml += '<div id="current_window_vlans"/>';
innerHtml += '<div id="current_window_errors"/>';
var content = document.createElement("div");
content.innerHTML = innerHtml;
showWindow(currentGraph, "Network Creation", content, 300, 300);
-
- if(vlans){
- vlan_notice = document.getElementById("current_window_vlans");
- vlan_notice.appendChild(document.createTextNode("Available Vlans: " + vlan_string));
- }
}
function parseNetworkWindow() {
var net_name = document.getElementById("net_name_input").value
- var vlan_id = document.getElementById("vlan_id_input").value
var error_div = document.getElementById("current_window_errors");
- var vlan_valid = Number.isInteger(Number(vlan_id)) && (vlan_id < 4095) && (vlan_id > 1)
- if(vlans){
- vlan_valid = vlan_valid & vlans.indexOf(Number(vlan_id)) >= 0;
- }
- if( !vlan_valid)
- {
- error_div.innerHTML = "Please only enter an integer in the valid range (default 1-4095) for the VLAN ID";
- return;
- }
- if( networks.has(vlan_id))
- {
- error_div.innerHTML = "All VLAN IDs must be unique";
- return;
- }
- if( network_names.has(net_name) ){
+ if( networks.has(net_name) ){
error_div.innerHTML = "All network names must be unique";
return;
}
- addNetwork(net_name, vlan_id);
+ addNetwork(net_name);
currentWindow.destroy();
}
@@ -312,6 +264,12 @@ function encodeGraph(graph) {
function doGlobalConfig(graph) {
//general graph stuff
graph.setMultigraph(false);
+ graph.setCellsSelectable(false);
+ graph.setCellsMovable(false);
+
+ //testing
+ graph.vertexLabelIsMovable = true;
+
//edge behavior
graph.setConnectable(true);
@@ -332,6 +290,9 @@ function doGlobalConfig(graph) {
style[mxConstants.STYLE_ROUNDED] = true;
style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation;
+ hostStyle = graph.getStylesheet().getDefaultVertexStyle();
+ hostStyle[mxConstants.STYLE_ROUNDED] = 1;
+
// TODO: Proper override
graph.convertValueToString = function(cell) {
try{
@@ -395,29 +356,21 @@ function othersUntagged(edgeID) {
var end1 = edge.getTerminal(true);
var end2 = edge.getTerminal(false);
- if( end1.getParent().getId().split('_')[0] == 'host' )
- {
+ if( end1.getParent().getId().split('_')[0] == 'host' ){
var netint = end1;
- }
- else
- {
+ } else {
var netint = end2;
}
var edges = netint.edges;
- for( var i=0; i < edges.length; i++ )
- {
- if( edges[i].getValue() )
- {
+ for( var i=0; i < edges.length; i++ ) {
+ if( edges[i].getValue() ) {
var tagged = JSON.parse(edges[i].getValue()).tagged;
- }
- else
- {
+ } else {
var tagged = true;
}
- if( !tagged )
- {
+ if( !tagged ) {
return true;
}
}
@@ -454,12 +407,11 @@ function parseVlanWindow(edgeID) {
break;
}
}
- //edge.setValue(cellValue);
currentGraph.refresh(edge);
closeWindow();
}
-function makeMxNetwork(vlan_id, net_name) {
+function makeMxNetwork(net_name, public = false) {
model = currentGraph.getModel();
width = 10;
height = 1700;
@@ -472,9 +424,8 @@ function makeMxNetwork(vlan_id, net_name) {
//alert(color);
}
var net_val = Object();
- net_val['vlan_id'] = vlan_id;
net_val['name'] = net_name;
- net_val['public'] = vlan_id < 0;
+ net_val['public'] = public;
net = currentGraph.insertVertex(
currentGraph.getDefaultParent(),
'network_' + netCount,
@@ -505,23 +456,23 @@ function makeMxNetwork(vlan_id, net_name) {
retVal['color'] = color;
retVal['element_id'] = "network_" + netCount;
+ networks.add(net_name);
+
netCount++;
return retVal;
}
function addPublicNetwork() {
- var net = makeMxNetwork(-1, "public");
- network_names.add("public");
- makeSidebarNetwork("public", "", net['color'], net['element_id']);
+ var net = makeMxNetwork("public", true);
+ makeSidebarNetwork("public", net['color'], net['element_id']);
+ has_public_net = true;
}
-function addNetwork(net_name, vlan_id) {
- var ret = makeMxNetwork(vlan_id, net_name);
+function addNetwork(net_name) {
+ var ret = makeMxNetwork(net_name);
var color = ret['color'];
var net_id = ret['element_id'];
- networks.add(vlan_id);
- network_names.add(net_name);
- makeSidebarNetwork(net_name, vlan_id, color, net_id);
+ makeSidebarNetwork(net_name, color, net_id);
}
function updateHosts(removed) {
@@ -535,8 +486,7 @@ function updateHosts(removed) {
var hosts = currentGraph.getChildVertices(currentGraph.getDefaultParent());
var topdist = 100;
- for(var i=0; i<hosts.length; i++)
- {
+ for(var i=0; i<hosts.length; i++) {
var host = hosts[i];
if(!host.id.startsWith("host_"))
{
@@ -549,7 +499,7 @@ function updateHosts(removed) {
}
}
-function makeSidebarNetwork(net_name, vlan_id, color, net_id){
+function makeSidebarNetwork(net_name, color, net_id){
var newNet = document.createElement("li");
var colorBlob = document.createElement("div");
colorBlob.className = "colorblob";
@@ -565,9 +515,6 @@ function makeSidebarNetwork(net_name, vlan_id, color, net_id){
createDeleteDialog(net_id);
}, false);
var text = net_name;
- if(vlan_id){
- text += " : " + vlan_id;
- }
var newNetValue = document.createTextNode(text);
textContainer.appendChild(newNetValue);
colorBlob.style['background'] = color;
@@ -585,7 +532,7 @@ function makeHost(hostInfo) {
interfaces = hostInfo['interfaces'];
graph = currentGraph;
width = 100;
- height = (25 * interfaces.length) + 10;
+ height = (25 * interfaces.length) + 25;
xoff = 75;
yoff = lastHostBottom + 50;
lastHostBottom = yoff + height;
@@ -600,6 +547,7 @@ function makeHost(hostInfo) {
'editable=0',
false
);
+ host.getGeometry().offset = new mxPoint(-50,0);
host.setConnectable(false);
hostCount++;
@@ -609,13 +557,16 @@ function makeHost(hostInfo) {
null,
JSON.stringify(interfaces[i]),
90,
- (i * 25) + 5,
+ (i * 25) + 12,
20,
20,
'fillColor=blue;editable=0',
false
);
+ port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0);
+ currentGraph.refresh(port);
}
+ currentGraph.refresh(host);
}
function submitForm() {
@@ -709,7 +660,6 @@ function submitForm() {
</div>
<ul id="network_list">
</ul>
- <p id="vlan_notice"></p>
<button type="button" style="display: none" onclick="submitForm();">Submit</button>
</div>
<form id="xml_form" method="post" action="/wf/workflow/">
diff --git a/dashboard/src/templates/workflow/viewport-base.html b/dashboard/src/templates/workflow/viewport-base.html
index f78bc01..1329595 100644
--- a/dashboard/src/templates/workflow/viewport-base.html
+++ b/dashboard/src/templates/workflow/viewport-base.html
@@ -84,12 +84,19 @@
background: #0FD57D;
}
- #viewport-iframe
- {
- height: calc(100vh - 450);
- }
-
+ .iframe_div {
+ width: calc(100% - 450px);
+ margin-left: 70px;
+ height: calc(100vh - 155px);
+ position: absolute;
+ border: none;
+ }
+ .iframe_elem {
+ width: 100%;
+ height: calc(100vh - 155px);
+ border: none;
+ }
</style>
<button id="gof" onclick="go(step+1)" class="btn go_btn go_forward">Go Forward</button>
@@ -415,15 +422,7 @@
document.getElementById("view_message").className = "step_message";
document.getElementById("view_message").classList.add("message_" + stepstatus);
}
- function resize_iframe(){
- var page_rect = document.getElementById("wrapper").getBoundingClientRect();
- var title_rect = document.getElementById("iframe_header").getBoundingClientRect();
- var iframe_height = page_rect.bottom - title_rect.bottom;
- document.getElementById("viewport-iframe").height = iframe_height;
- }
- window.addEventListener('load', resize_iframe);
- window.addEventListener('resize', resize_iframe);
</script>
<!-- /.col-lg-12 -->
</div>
@@ -433,5 +432,7 @@
</form>
</div>
-<iframe src="/wf/workflow" style="position: absolute; left: 351px; right: 105px; width: calc(100% - 450px); border-style: none; border-width: 1px; border-color: #888888;" scrolling="yes" id="viewport-iframe" onload="resize_iframe();"></iframe>
+<div class="iframe_div">
+ <iframe src="/wf/workflow" class="iframe_elem" scrolling="yes" id="viewport-iframe"></iframe>
+</div>
{% endblock content %}
diff --git a/dashboard/src/workflow/forms.py b/dashboard/src/workflow/forms.py
index b40713f..6d26b5c 100644
--- a/dashboard/src/workflow/forms.py
+++ b/dashboard/src/workflow/forms.py
@@ -20,9 +20,8 @@ from resource_inventory.models import (
GenericResourceBundle,
ConfigBundle,
OPNFVRole,
- Image,
Installer,
- Scenario
+ Scenario,
)
@@ -125,6 +124,7 @@ class SWConfigSelectorForm(forms.Form):
bundle = None
edit = False
resource = None
+ user = None
if "chosen_software" in kwargs:
chosen_software = kwargs.pop("chosen_software")
@@ -134,18 +134,25 @@ class SWConfigSelectorForm(forms.Form):
edit = kwargs.pop("edit")
if "resource" in kwargs:
resource = kwargs.pop("resource")
+ if "user" in kwargs:
+ user = kwargs.pop("user")
super(SWConfigSelectorForm, self).__init__(*args, **kwargs)
- attrs = self.build_search_widget_attrs(chosen_software, bundle, edit, resource)
+ attrs = self.build_search_widget_attrs(chosen_software, bundle, edit, resource, user)
self.fields['software_bundle'] = forms.CharField(
widget=SearchableSelectMultipleWidget(attrs=attrs)
)
- def build_search_widget_attrs(self, chosen, bundle, edit, resource):
+ def build_search_widget_attrs(self, chosen, bundle, edit, resource, user):
configs = {}
queryset = ConfigBundle.objects.select_related('owner').all()
if resource:
+ if user is None:
+ user = resource.owner
queryset = queryset.filter(bundle=resource)
+ if user:
+ queryset = queryset.filter(owner=user)
+
for config in queryset:
displayable = {}
displayable['small_name'] = config.name
@@ -424,20 +431,14 @@ class NetworkConfigurationForm(forms.Form):
class HostSoftwareDefinitionForm(forms.Form):
- fields = ["host_name", "role", "image"]
host_name = forms.CharField(max_length=200, disabled=True, required=False)
- role = forms.ModelChoiceField(queryset=OPNFVRole.objects.all())
- image = forms.ModelChoiceField(queryset=Image.objects.all())
-
+ headnode = forms.BooleanField(required=False, widget=forms.HiddenInput)
-class SoftwareConfigurationForm(forms.Form):
-
- name = forms.CharField(max_length=200)
- description = forms.CharField(widget=forms.Textarea)
- opnfv = forms.BooleanField(disabled=True, required=False)
- installer = forms.ModelChoiceField(queryset=Installer.objects.all(), disabled=True, required=False)
- scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), disabled=True, required=False)
+ def __init__(self, *args, **kwargs):
+ imageQS = kwargs.pop("imageQS")
+ super(HostSoftwareDefinitionForm, self).__init__(*args, **kwargs)
+ self.fields['image'] = forms.ModelChoiceField(queryset=imageQS)
class WorkflowSelectionForm(forms.Form):
@@ -461,7 +462,7 @@ class SnapshotHostSelectForm(forms.Form):
host = forms.CharField()
-class SnapshotMetaForm(forms.Form):
+class BasicMetaForm(forms.Form):
name = forms.CharField()
description = forms.CharField(widget=forms.Textarea)
@@ -475,3 +476,23 @@ class ConfirmationForm(forms.Form):
(False, "Cancel")
)
)
+
+
+class OPNFVSelectionForm(forms.Form):
+ installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=True)
+ scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=True)
+
+
+class OPNFVNetworkRoleForm(forms.Form):
+ role = forms.CharField(max_length=200, disabled=True, required=False)
+
+ def __init__(self, *args, config_bundle, **kwargs):
+ super(OPNFVNetworkRoleForm, self).__init__(*args, **kwargs)
+ self.fields['network'] = forms.ModelChoiceField(
+ queryset=config_bundle.bundle.networks.all()
+ )
+
+
+class OPNFVHostRoleForm(forms.Form):
+ host_name = forms.CharField(max_length=200, disabled=True, required=False)
+ role = forms.ModelChoiceField(queryset=OPNFVRole.objects.all().order_by("name").distinct("name"))
diff --git a/dashboard/src/workflow/models.py b/dashboard/src/workflow/models.py
index cdfddef..bf5751d 100644
--- a/dashboard/src/workflow/models.py
+++ b/dashboard/src/workflow/models.py
@@ -19,7 +19,7 @@ import requests
from workflow.forms import ConfirmationForm
from api.models import JobFactory
from dashboard.exceptions import ResourceAvailabilityException, ModelValidationException
-from resource_inventory.models import Image, GenericInterface
+from resource_inventory.models import Image, GenericInterface, OPNFVConfig, HostOPNFVConfig, NetworkRole
from resource_inventory.resource_manager import ResourceManager
from resource_inventory.pdf_templater import PDFTemplater
from notifier.manager import NotificationHandler
@@ -143,10 +143,12 @@ class BookingAuthManager():
currently checks if the booking uses multiple servers. if it does, then the owner must be a PTL,
which is checked using the provided info file
"""
- if len(booking.resource.template.getHosts()) < 2:
- return True # if they only have one server, we dont care
if booking.owner.userprofile.booking_privledge:
return True # admin override for this user
+ if Booking.objects.filter(owner=booking.owner, end__gt=timezone.now()).count() >= 3:
+ return False
+ if len(booking.resource.template.getHosts()) < 2:
+ return True # if they only have one server, we dont care
if repo.BOOKING_INFO_FILE not in repo.el:
return False # INFO file not provided
ptl_info = self.parse_url(repo.el.get(repo.BOOKING_INFO_FILE))
@@ -203,27 +205,6 @@ class Confirmation_Step(WorkflowStep):
title = "Confirm Changes"
description = "Does this all look right?"
- def get_vlan_warning(self):
- grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE, False)
- if not grb:
- return 0
- if self.repo.BOOKING_MODELS not in self.repo.el:
- return 0
- vlan_manager = grb.lab.vlan_manager
- if vlan_manager is None:
- return 0
- hosts = grb.getHosts()
- for host in hosts:
- for interface in host.generic_interfaces.all():
- for vlan in interface.vlans.all():
- if vlan.public:
- if not vlan_manager.public_vlan_is_available(vlan.vlan_id):
- return 1
- else:
- if not vlan_manager.is_available(vlan.vlan_id):
- return 1 # There is a problem with these vlans
- return 0
-
def get_context(self):
context = super(Confirmation_Step, self).get_context()
context['form'] = ConfirmationForm()
@@ -231,7 +212,6 @@ class Confirmation_Step(WorkflowStep):
self.repo_get(self.repo.CONFIRMATION),
default_flow_style=False
).strip()
- context['vlan_warning'] = self.get_vlan_warning()
return context
@@ -262,33 +242,8 @@ class Confirmation_Step(WorkflowStep):
pass
else:
- if "vlan_input" in request.POST:
- if request.POST.get("vlan_input") == "True":
- self.translate_vlans()
- return self.render(request)
pass
- def translate_vlans(self):
- grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE, False)
- if not grb:
- return 0
- vlan_manager = grb.lab.vlan_manager
- if vlan_manager is None:
- return 0
- hosts = grb.getHosts()
- for host in hosts:
- for interface in host.generic_interfaces.all():
- for vlan in interface.vlans.all():
- if not vlan.public:
- if not vlan_manager.is_available(vlan.vlan_id):
- vlan.vlan_id = vlan_manager.get_vlan()
- vlan.save()
- else:
- if not vlan_manager.public_vlan_is_available(vlan.vlan_id):
- pub_vlan = vlan_manager.get_public_vlan()
- vlan.vlan_id = pub_vlan.vlan
- vlan.save()
-
class Workflow():
@@ -304,6 +259,7 @@ class Repository():
CONFIRMATION = "confirmation"
SELECTED_GRESOURCE_BUNDLE = "selected generic bundle pk"
SELECTED_CONFIG_BUNDLE = "selected config bundle pk"
+ SELECTED_OPNFV_CONFIG = "selected opnfv deployment config"
GRESOURCE_BUNDLE_MODELS = "generic_resource_bundle_models"
GRESOURCE_BUNDLE_INFO = "generic_resource_bundle_info"
BOOKING = "booking"
@@ -313,6 +269,7 @@ class Repository():
SWCONF_HOSTS = "swconf_hosts"
BOOKING_MODELS = "booking models"
CONFIG_MODELS = "configuration bundle models"
+ OPNFV_MODELS = "opnfv configuration models"
SESSION_USER = "session owner user account"
VALIDATED_MODEL_GRB = "valid grb config model instance in db"
VALIDATED_MODEL_CONFIG = "valid config model instance in db"
@@ -384,6 +341,14 @@ class Repository():
self.el[self.RESULT_KEY] = self.SELECTED_CONFIG_BUNDLE
return
+ if self.OPNFV_MODELS in self.el:
+ errors = self.make_opnfv_config()
+ if errors:
+ return errors
+ else:
+ self.el[self.HAS_RESULT] = True
+ self.el[self.RESULT_KEY] = self.SELECTED_OPNFV_CONFIG
+
if self.BOOKING_MODELS in self.el:
errors = self.make_booking()
if errors:
@@ -453,6 +418,11 @@ class Repository():
except Exception as e:
return "GRB, saving hosts generated exception: " + str(e) + " CODE:0x0005"
+ if 'networks' in models:
+ for net in models['networks'].values():
+ net.bundle = bundle
+ net.save()
+
if 'interfaces' in models:
for interface_set in models['interfaces'].values():
for interface in interface_set:
@@ -464,20 +434,21 @@ class Repository():
else:
return "GRB, no interface set provided. CODE:0x001a"
- if 'vlans' in models:
- for resource_name, mapping in models['vlans'].items():
- for profile_name, vlan_set in mapping.items():
+ if 'connections' in models:
+ for resource_name, mapping in models['connections'].items():
+ for profile_name, connection_set in mapping.items():
interface = GenericInterface.objects.get(
profile__name=profile_name,
host__resource__name=resource_name,
host__resource__bundle=models['bundle']
)
- for vlan in vlan_set:
+ for connection in connection_set:
try:
- vlan.save()
- interface.vlans.add(vlan)
+ connection.network = connection.network
+ connection.save()
+ interface.connections.add(connection)
except Exception as e:
- return "GRB, saving vlan " + str(vlan) + " failed. Exception: " + str(e) + ". CODE:0x0017"
+ return "GRB, saving vlan " + str(connection) + " failed. Exception: " + str(e) + ". CODE:0x0017"
else:
return "GRB, no vlan set provided. CODE:0x0018"
@@ -534,9 +505,6 @@ class Repository():
else:
return "BOOK, no selected resource. CODE:0x000e"
- if not self.reserve_vlans(selected_grb):
- return "BOOK, vlans not available"
-
if 'booking' in models:
booking = models['booking']
else:
@@ -578,7 +546,7 @@ class Repository():
booking.collaborators.add(collaborator)
try:
- booking.pdf = PDFTemplater.makePDF(booking.resource)
+ booking.pdf = PDFTemplater.makePDF(booking)
booking.save()
except Exception as e:
return "BOOK, failed to create Pod Desriptor File: " + str(e)
@@ -593,29 +561,52 @@ class Repository():
except Exception as e:
return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0016"
- def reserve_vlans(self, grb):
- """
- True is success
- """
- vlans = []
- public_vlan = None
- vlan_manager = grb.lab.vlan_manager
- if vlan_manager is None:
- return True
- for host in grb.getHosts():
- for interface in host.generic_interfaces.all():
- for vlan in interface.vlans.all():
- if vlan.public:
- public_vlan = vlan
- else:
- vlans.append(vlan.vlan_id)
-
- try:
- vlan_manager.reserve_vlans(vlans)
- vlan_manager.reserve_public_vlan(public_vlan.vlan_id)
- return True
- except Exception:
- return False
+ def make_opnfv_config(self):
+ opnfv_models = self.el[self.OPNFV_MODELS]
+ config_bundle = opnfv_models['configbundle']
+ if not config_bundle:
+ return "No Configuration bundle selected"
+ info = opnfv_models.get("meta", {})
+ name = info.get("name", False)
+ desc = info.get("description", False)
+ if not (name and desc):
+ return "No name or description given"
+ installer = opnfv_models['installer_chosen']
+ if not installer:
+ return "No OPNFV Installer chosen"
+ scenario = opnfv_models['scenario_chosen']
+ if not scenario:
+ return "No OPNFV Scenario chosen"
+
+ opnfv_config = OPNFVConfig.objects.create(
+ bundle=config_bundle,
+ name=name,
+ description=desc,
+ installer=installer,
+ scenario=scenario
+ )
+
+ network_roles = opnfv_models['network_roles']
+ for net_role in network_roles:
+ opnfv_config.networks.add(
+ NetworkRole.objects.create(
+ name=net_role['role'],
+ network=net_role['network']
+ )
+ )
+
+ host_roles = opnfv_models['host_roles']
+ for host_role in host_roles:
+ config = config_bundle.hostConfigurations.get(
+ host__resource__name=host_role['host_name']
+ )
+ HostOPNFVConfig.objects.create(
+ role=host_role['role'],
+ host_config=config,
+ opnfv_config=opnfv_config
+ )
+
+ self.el[self.RESULT] = opnfv_config
def __init__(self):
self.el = {}
diff --git a/dashboard/src/workflow/opnfv_workflow.py b/dashboard/src/workflow/opnfv_workflow.py
new file mode 100644
index 0000000..26e1d7c
--- /dev/null
+++ b/dashboard/src/workflow/opnfv_workflow.py
@@ -0,0 +1,327 @@
+##############################################################################
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.forms import formset_factory
+from django.contrib import messages
+
+import json
+
+from workflow.models import WorkflowStep
+from resource_inventory.models import ConfigBundle, OPNFV_SETTINGS
+from workflow.forms import OPNFVSelectionForm, OPNFVNetworkRoleForm, OPNFVHostRoleForm, SWConfigSelectorForm, BasicMetaForm
+
+
+class OPNFV_Resource_Select(WorkflowStep):
+ template = 'booking/steps/swconfig_select.html'
+ title = "Select Software Configuration"
+ description = "Choose the software and related configurations you want to use to configure OPNFV"
+ short_title = "software configuration"
+ modified_key = "configbundle_step"
+
+ def update_confirmation(self):
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ config_bundle = self.repo_get(self.repo.OPNFV_MODELS, {}).get("configbundle")
+ if not config_bundle:
+ return
+ confirm['software bundle'] = config_bundle.name
+ confirm['hardware POD'] = config_bundle.bundle.name
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ def post_render(self, request):
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ form = SWConfigSelectorForm(request.POST)
+ if form.is_valid():
+ bundle_json = form.cleaned_data['software_bundle']
+ bundle_json = bundle_json[2:-2] # Stupid django string bug
+ if not bundle_json:
+ self.metastep.set_invalid("Please select a valid config")
+ return self.render(request)
+ bundle_json = json.loads(bundle_json)
+ if len(bundle_json) < 1:
+ self.metastep.set_invalid("Please select a valid config")
+ return self.render(request)
+ bundle = None
+ id = int(bundle_json[0]['id'])
+ bundle = ConfigBundle.objects.get(id=id)
+
+ models['configbundle'] = bundle
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ self.metastep.set_valid("Step Completed")
+ messages.add_message(request, messages.SUCCESS, 'Form Validated Successfully', fail_silently=True)
+ self.update_confirmation()
+ else:
+ self.metastep.set_invalid("Please select or create a valid config")
+ messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True)
+
+ return self.render(request)
+
+ def get_context(self):
+ context = super(OPNFV_Resource_Select, self).get_context()
+ default = []
+ user = self.repo_get(self.repo.SESSION_USER)
+
+ context['form'] = SWConfigSelectorForm(chosen_software=default, bundle=None, edit=True, resource=None, user=user)
+ return context
+
+
+class Pick_Installer(WorkflowStep):
+ template = 'config_bundle/steps/pick_installer.html'
+ title = 'Pick OPNFV Installer'
+ description = 'Choose which OPNFV installer to use'
+ short_title = "opnfv installer"
+ modified_key = "installer_step"
+
+ def update_confirmation(self):
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ installer = models.get("installer_chosen")
+ scenario = models.get("scenario_chosen")
+ if not (installer and scenario):
+ return
+ confirm['installer'] = installer.name
+ confirm['scenario'] = scenario.name
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ def get_context(self):
+ context = super(Pick_Installer, self).get_context()
+
+ models = self.repo_get(self.repo.OPNFV_MODELS, None)
+ initial = {
+ "installer": models.get("installer_chosen"),
+ "scenario": models.get("scenario_chosen")
+ }
+
+ context["form"] = OPNFVSelectionForm(initial=initial)
+ return context
+
+ def post_render(self, request):
+ form = OPNFVSelectionForm(request.POST)
+ if form.is_valid():
+ installer = form.cleaned_data['installer']
+ scenario = form.cleaned_data['scenario']
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ models['installer_chosen'] = installer
+ models['scenario_chosen'] = scenario
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ self.update_confirmation()
+ self.metastep.set_valid("Step Completed")
+ else:
+ self.metastep.set_invalid("Please select an Installer and Scenario")
+
+ return self.render(request)
+
+
+class Assign_Network_Roles(WorkflowStep):
+ template = 'config_bundle/steps/assign_network_roles.html'
+ title = 'Pick Network Roles'
+ description = 'Choose what role each network should get'
+ short_title = "network roles"
+ modified_key = "net_roles_step"
+
+ """
+ to do initial filling, repo should have a "network_roles" array with the following structure for each element:
+ {
+ "role": <NetworkRole object ref>,
+ "network": <Network object ref>
+ }
+ """
+ def create_netformset(self, roles, config_bundle, data=None):
+ roles_initial = []
+ set_roles = self.repo_get(self.repo.OPNFV_MODELS, {}).get("network_roles")
+ if set_roles:
+ roles_initial = set_roles
+ else:
+ for role in OPNFV_SETTINGS.NETWORK_ROLES:
+ roles_initial.append({"role": role})
+
+ Formset = formset_factory(OPNFVNetworkRoleForm, extra=0)
+ kwargs = {
+ "initial": roles_initial,
+ "form_kwargs": {"config_bundle": config_bundle}
+ }
+ formset = None
+ if data:
+ formset = Formset(data, **kwargs)
+ else:
+ formset = Formset(**kwargs)
+ return formset
+
+ def get_context(self):
+ context = super(Assign_Network_Roles, self).get_context()
+ config_bundle = self.repo_get(self.repo.OPNFV_MODELS, {}).get("configbundle")
+ if config_bundle is None:
+ context["unavailable"] = True
+ return context
+
+ roles = OPNFV_SETTINGS.NETWORK_ROLES
+ formset = self.create_netformset(roles, config_bundle)
+ context['formset'] = formset
+
+ return context
+
+ def update_confirmation(self):
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ roles = models.get("network_roles")
+ if not roles:
+ return
+ confirm['network roles'] = {}
+ for role in roles:
+ confirm['network roles'][role['role']] = role['network'].name
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ def post_render(self, request):
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ config_bundle = models.get("configbundle")
+ roles = OPNFV_SETTINGS.NETWORK_ROLES
+ net_role_formset = self.create_netformset(roles, config_bundle, data=request.POST)
+ if net_role_formset.is_valid():
+ results = []
+ for form in net_role_formset:
+ results.append({
+ "role": form.cleaned_data['role'],
+ "network": form.cleaned_data['network']
+ })
+ models['network_roles'] = results
+ self.metastep.set_valid("Completed")
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ self.update_confirmation()
+ else:
+ self.metastep.set_invalid("Please complete all fields")
+ return self.render(request)
+
+
+class Assign_Host_Roles(WorkflowStep): # taken verbatim from Define_Software in sw workflow, merge the two?
+ template = 'config_bundle/steps/assign_host_roles.html'
+ title = 'Pick Host Roles'
+ description = "Choose the role each machine will have in your OPNFV pod"
+ short_title = "host roles"
+ modified_key = "host_roles_step"
+
+ def create_host_role_formset(self, hostlist=[], data=None):
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ host_roles = models.get("host_roles", [])
+ if not host_roles:
+ for host in hostlist:
+ initial = {"host_name": host.resource.name}
+ host_roles.append(initial)
+ models['host_roles'] = host_roles
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+
+ HostFormset = formset_factory(OPNFVHostRoleForm, extra=0)
+
+ kwargs = {"initial": host_roles}
+ formset = None
+ if data:
+ formset = HostFormset(data, **kwargs)
+ else:
+ formset = HostFormset(**kwargs)
+
+ return formset
+
+ def get_context(self):
+ context = super(Assign_Host_Roles, self).get_context()
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ config = models.get("configbundle")
+ if config is None:
+ context['error'] = "Please select a Configuration on the first step"
+
+ formset = self.create_host_role_formset(hostlist=config.bundle.getHosts())
+ context['formset'] = formset
+
+ return context
+
+ def get_host_role_mapping(self, host_roles, hostname):
+ for obj in host_roles:
+ if hostname == obj['host_name']:
+ return obj
+ return None
+
+ def update_confirmation(self):
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ roles = models.get("host_roles")
+ if not roles:
+ return
+ confirm['host roles'] = {}
+ for role in roles:
+ confirm['host roles'][role['host_name']] = role['role'].name
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ def post_render(self, request):
+ formset = self.create_host_role_formset(data=request.POST)
+
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ host_roles = models.get("host_roles", [])
+
+ has_jumphost = False
+ if formset.is_valid():
+ for form in formset:
+ hostname = form.cleaned_data['host_name']
+ role = form.cleaned_data['role']
+ mapping = self.get_host_role_mapping(host_roles, hostname)
+ mapping['role'] = role
+ if "jumphost" in role.name.lower():
+ has_jumphost = True
+
+ models['host_roles'] = host_roles
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ self.update_confirmation()
+
+ if not has_jumphost:
+ self.metastep.set_invalid('Must have at least one "Jumphost" per POD')
+ else:
+ self.metastep.set_valid("Completed")
+ else:
+ self.metastep.set_invalid("Please complete all fields")
+
+ return self.render(request)
+
+
+class MetaInfo(WorkflowStep):
+ template = 'config_bundle/steps/config_software.html'
+ title = "Other Info"
+ description = "Give your software config a name, description, and other stuff"
+ short_title = "config info"
+
+ def get_context(self):
+ context = super(MetaInfo, self).get_context()
+
+ initial = self.repo_get(self.repo.OPNFV_MODELS, {}).get("meta", {})
+ context["form"] = BasicMetaForm(initial=initial)
+ return context
+
+ def update_confirmation(self):
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ meta = models.get("meta")
+ if not meta:
+ return
+ confirm['name'] = meta['name']
+ confirm['description'] = meta['description']
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ def post_render(self, request):
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ info = models.get("meta", {})
+
+ form = BasicMetaForm(request.POST)
+ if form.is_valid():
+ info['name'] = form.cleaned_data['name']
+ info['description'] = form.cleaned_data['description']
+ models['meta'] = info
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ self.update_confirmation()
+ self.metastep.set_valid("Complete")
+ else:
+ self.metastep.set_invalid("Please correct the errors shown below")
+
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ return self.render(request)
diff --git a/dashboard/src/workflow/resource_bundle_workflow.py b/dashboard/src/workflow/resource_bundle_workflow.py
index 4858ebe..536187f 100644
--- a/dashboard/src/workflow/resource_bundle_workflow.py
+++ b/dashboard/src/workflow/resource_bundle_workflow.py
@@ -10,6 +10,7 @@
from django.shortcuts import render
from django.forms import formset_factory
+from django.conf import settings
import json
import re
@@ -25,11 +26,12 @@ from workflow.forms import (
)
from resource_inventory.models import (
GenericResourceBundle,
- Vlan,
GenericInterface,
GenericHost,
GenericResource,
- HostProfile
+ HostProfile,
+ Network,
+ NetworkConnection
)
from dashboard.exceptions import (
InvalidVlanConfigurationException,
@@ -185,6 +187,7 @@ class Define_Nets(WorkflowStep):
hostlist = self.repo_get(self.repo.GRB_LAST_HOSTLIST, None)
added_list = []
added_dict = {}
+ context['debug'] = settings.DEBUG
context['added_hosts'] = []
if hostlist is not None:
new_hostlist = []
@@ -239,15 +242,15 @@ class Define_Nets(WorkflowStep):
self.metastep.set_valid("Networks applied successfully")
except ResourceAvailabilityException:
self.metastep.set_invalid("Public network not availble")
- except Exception:
- self.metastep.set_invalid("An error occurred when applying networks")
+ except Exception as e:
+ self.metastep.set_invalid("An error occurred when applying networks: " + str(e))
return self.render(request)
def updateModels(self, xmlData):
models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
- models["vlans"] = {}
- given_hosts, interfaces = self.parseXml(xmlData)
- vlan_manager = models['bundle'].lab.vlan_manager
+ models["connections"] = {}
+ models['networks'] = {}
+ given_hosts, interfaces, networks = self.parseXml(xmlData)
existing_host_list = models.get("hosts", [])
existing_hosts = {} # maps id to host
for host in existing_host_list:
@@ -255,104 +258,133 @@ class Define_Nets(WorkflowStep):
bundle = models.get("bundle", GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER)))
+ for net_id, net in networks.items():
+ network = Network()
+ network.name = net['name']
+ network.bundle = bundle
+ network.is_public = net['public']
+ models['networks'][net_id] = network
+
for hostid, given_host in given_hosts.items():
existing_host = existing_hosts[hostid[5:]]
for ifaceId in given_host['interfaces']:
iface = interfaces[ifaceId]
- if existing_host.resource.name not in models['vlans']:
- models['vlans'][existing_host.resource.name] = {}
- models['vlans'][existing_host.resource.name][iface['profile_name']] = []
- for network in iface['networks']:
- vlan_id = network['network']['vlan']
- is_public = network['network']['public']
- if is_public:
- public_net = vlan_manager.get_public_vlan()
- if public_net is None:
- raise ResourceAvailabilityException("No public networks available")
- vlan_id = vlan_manager.get_public_vlan().vlan
- vlan = Vlan(vlan_id=vlan_id, tagged=network['tagged'], public=is_public)
- models['vlans'][existing_host.resource.name][iface['profile_name']].append(vlan)
+ if existing_host.resource.name not in models['connections']:
+ models['connections'][existing_host.resource.name] = {}
+ models['connections'][existing_host.resource.name][iface['profile_name']] = []
+ for connection in iface['connections']:
+ network_id = connection['network']
+ net = models['networks'][network_id]
+ connection = NetworkConnection(vlan_is_tagged=connection['tagged'], network=net)
+ models['connections'][existing_host.resource.name][iface['profile_name']].append(connection)
bundle.xml = xmlData
self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models)
- # serialize and deserialize xml from mxGraph
- def parseXml(self, xmlString):
- parent_nets = {} # map network ports to networks
- networks = {} # maps net id to network object
- hosts = {} # cotains id -> hosts, each containing interfaces, referencing networks
- interfaces = {} # maps id -> interface
+ def decomposeXml(self, xmlString):
+ """
+ This function takes in an xml doc from our front end
+ and returns dictionaries that map cellIds to the xml
+ nodes themselves. There is no unpacking of the
+ xml objects, just grouping and organizing
+ """
+
+ connections = {}
+ networks = {}
+ hosts = {}
+ interfaces = {}
+ network_ports = {}
+
xmlDom = minidom.parseString(xmlString)
root = xmlDom.documentElement.firstChild
- netids = {}
- untagged_ints = {}
for cell in root.childNodes:
cellId = cell.getAttribute('id')
+ group = cellId.split("_")[0]
+ parentGroup = cell.getAttribute("parent").split("_")[0]
+ # place cell into correct group
if cell.getAttribute("edge"):
- # cell is a network connection
- escaped_json_str = cell.getAttribute("value")
- json_str = escaped_json_str.replace('&quot;', '"')
- attributes = json.loads(json_str)
- tagged = attributes['tagged']
- interface = None
- network = None
- src = cell.getAttribute("source")
- tgt = cell.getAttribute("target")
- if src in parent_nets:
- # src is a network port
- network = networks[parent_nets[src]]
- if tgt in untagged_ints and not tagged:
- raise InvalidVlanConfigurationException("More than one untagged vlan on an interface")
- interface = interfaces[tgt]
- untagged_ints[tgt] = True
- else:
- network = networks[parent_nets[tgt]]
- if src in untagged_ints and not tagged:
- raise InvalidVlanConfigurationException("More than one untagged vlan on an interface")
- interface = interfaces[src]
- untagged_ints[src] = True
- interface['networks'].append({"network": network, "tagged": tagged})
-
- elif "network" in cellId: # cell is a network
- escaped_json_str = cell.getAttribute("value")
- json_str = escaped_json_str.replace('&quot;', '"')
- net_info = json.loads(json_str)
- nid = net_info['vlan_id']
- public = net_info['public']
- try:
- int_netid = int(nid)
- assert public or int_netid > 1, "Net id is 1 or lower"
- assert int_netid < 4095, "Net id is 4095 or greater"
- except Exception:
- raise InvalidVlanConfigurationException("VLAN ID is not an integer more than 1 and less than 4095")
- if nid in netids:
- raise NetworkExistsException("Non unique network id found")
- else:
- pass
- network = {"name": net_info['name'], "vlan": net_info['vlan_id'], "public": public}
- netids[net_info['vlan_id']] = True
- networks[cellId] = network
-
- elif "host" in cellId: # cell is a host/machine
- # TODO gather host info
- cell_json_str = cell.getAttribute("value")
- cell_json = json.loads(cell_json_str)
- host = {"interfaces": [], "name": cellId, "profile_name": cell_json['name']}
- hosts[cellId] = host
-
- elif cell.hasAttribute("parent"):
- parentId = cell.getAttribute('parent')
- if "network" in parentId:
- parent_nets[cellId] = parentId
- elif "host" in parentId:
- # TODO gather iface info
- cell_json_str = cell.getAttribute("value")
- cell_json = json.loads(cell_json_str)
- iface = {"name": cellId, "networks": [], "profile_name": cell_json['name']}
- hosts[parentId]['interfaces'].append(cellId)
- interfaces[cellId] = iface
- return hosts, interfaces
+ connections[cellId] = cell
+
+ elif "network" in group:
+ networks[cellId] = cell
+
+ elif "host" in group:
+ hosts[cellId] = cell
+
+ elif "host" in parentGroup:
+ interfaces[cellId] = cell
+
+ # make network ports also map to thier network
+ elif "network" in parentGroup:
+ network_ports[cellId] = cell.getAttribute("parent") # maps port ID to net ID
+
+ return connections, networks, hosts, interfaces, network_ports
+
+ # serialize and deserialize xml from mxGraph
+ def parseXml(self, xmlString):
+ networks = {} # maps net name to network object
+ hosts = {} # cotains id -> hosts, each containing interfaces, referencing networks
+ interfaces = {} # maps id -> interface
+ untagged_ifaces = set() # used to check vlan config
+ network_names = set() # used to check network names
+ xml_connections, xml_nets, xml_hosts, xml_ifaces, xml_ports = self.decomposeXml(xmlString)
+
+ # parse Hosts
+ for cellId, cell in xml_hosts.items():
+ cell_json_str = cell.getAttribute("value")
+ cell_json = json.loads(cell_json_str)
+ host = {"interfaces": [], "name": cellId, "profile_name": cell_json['name']}
+ hosts[cellId] = host
+
+ # parse networks
+ for cellId, cell in xml_nets.items():
+ escaped_json_str = cell.getAttribute("value")
+ json_str = escaped_json_str.replace('&quot;', '"')
+ net_info = json.loads(json_str)
+ net_name = net_info['name']
+ public = net_info['public']
+ if net_name in network_names:
+ raise NetworkExistsException("Non unique network name found")
+ network = {"name": net_name, "public": public, "id": cellId}
+ networks[cellId] = network
+ network_names.add(net_name)
+
+ # parse interfaces
+ for cellId, cell in xml_ifaces.items():
+ parentId = cell.getAttribute('parent')
+ cell_json_str = cell.getAttribute("value")
+ cell_json = json.loads(cell_json_str)
+ iface = {"name": cellId, "connections": [], "profile_name": cell_json['name']}
+ hosts[parentId]['interfaces'].append(cellId)
+ interfaces[cellId] = iface
+
+ # parse connections
+ for cellId, cell in xml_connections.items():
+ escaped_json_str = cell.getAttribute("value")
+ json_str = escaped_json_str.replace('&quot;', '"')
+ attributes = json.loads(json_str)
+ tagged = attributes['tagged']
+ interface = None
+ network = None
+ src = cell.getAttribute("source")
+ tgt = cell.getAttribute("target")
+ if src in interfaces:
+ interface = interfaces[src]
+ network = networks[xml_ports[tgt]]
+ else:
+ interface = interfaces[tgt]
+ network = networks[xml_ports[src]]
+
+ if not tagged:
+ if interface['name'] in untagged_ifaces:
+ raise InvalidVlanConfigurationException("More than one untagged vlan on an interface")
+ untagged_ifaces.add(interface['name'])
+
+ # add connection to interface
+ interface['connections'].append({"tagged": tagged, "network": network['id']})
+
+ return hosts, interfaces, networks
class Resource_Meta_Info(WorkflowStep):
diff --git a/dashboard/src/workflow/snapshot_workflow.py b/dashboard/src/workflow/snapshot_workflow.py
index 002aee5..34ac3a5 100644
--- a/dashboard/src/workflow/snapshot_workflow.py
+++ b/dashboard/src/workflow/snapshot_workflow.py
@@ -14,7 +14,7 @@ import json
from booking.models import Booking
from resource_inventory.models import Host, Image
from workflow.models import WorkflowStep
-from workflow.forms import SnapshotMetaForm, SnapshotHostSelectForm
+from workflow.forms import BasicMetaForm, SnapshotHostSelectForm
class Select_Host_Step(WorkflowStep):
@@ -91,14 +91,14 @@ class Image_Meta_Step(WorkflowStep):
desc = self.repo_get(self.repo.SNAPSHOT_DESC, False)
form = None
if name and desc:
- form = SnapshotMetaForm(initial={"name": name, "description": desc})
+ form = BasicMetaForm(initial={"name": name, "description": desc})
else:
- form = SnapshotMetaForm()
+ form = BasicMetaForm()
context['form'] = form
return context
def post_render(self, request):
- form = SnapshotMetaForm(request.POST)
+ form = BasicMetaForm(request.POST)
if form.is_valid():
name = form.cleaned_data['name']
self.repo_put(self.repo.SNAPSHOT_NAME, name)
diff --git a/dashboard/src/workflow/sw_bundle_workflow.py b/dashboard/src/workflow/sw_bundle_workflow.py
index fd41018..a6a7464 100644
--- a/dashboard/src/workflow/sw_bundle_workflow.py
+++ b/dashboard/src/workflow/sw_bundle_workflow.py
@@ -11,9 +11,9 @@
from django.forms import formset_factory
from workflow.models import WorkflowStep
-from workflow.forms import SoftwareConfigurationForm, HostSoftwareDefinitionForm
+from workflow.forms import BasicMetaForm, HostSoftwareDefinitionForm
from workflow.booking_workflow import Resource_Select
-from resource_inventory.models import Image, GenericHost, ConfigBundle, HostConfiguration, Installer, OPNFVConfig
+from resource_inventory.models import Image, GenericHost, ConfigBundle, HostConfiguration
# resource selection step is reused from Booking workflow
@@ -39,48 +39,57 @@ class Define_Software(WorkflowStep):
description = "Choose the opnfv and image of your machines"
short_title = "host config"
- def create_hostformset(self, hostlist):
+ def build_filter_data(self, hosts_data):
+ """
+ returns a 2D array of images to exclude
+ based on the ordering of the passed
+ hosts_data
+ """
+ filter_data = []
+ user = self.repo_get(self.repo.SESSION_USER)
+ lab = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE).lab
+ for i, host_data in enumerate(hosts_data):
+ host = GenericHost.objects.get(pk=host_data['host_id'])
+ wrong_owner = Image.objects.exclude(owner=user).exclude(public=True)
+ wrong_host = Image.objects.exclude(host_type=host.profile)
+ wrong_lab = Image.objects.exclude(from_lab=lab)
+ excluded_images = wrong_owner | wrong_host | wrong_lab
+ filter_data.append([])
+ for image in excluded_images:
+ filter_data[i].append(image.pk)
+ return filter_data
+
+ def create_hostformset(self, hostlist, data=None):
hosts_initial = []
host_configs = self.repo_get(self.repo.CONFIG_MODELS, {}).get("host_configs", False)
if host_configs:
for config in host_configs:
- host_initial = {'host_id': config.host.id, 'host_name': config.host.resource.name}
- host_initial['role'] = config.opnfvRole
- host_initial['image'] = config.image
- hosts_initial.append(host_initial)
-
+ hosts_initial.append({
+ 'host_id': config.host.id,
+ 'host_name': config.host.resource.name,
+ 'headnode': config.is_head_node,
+ 'image': config.image
+ })
else:
for host in hostlist:
- host_initial = {'host_id': host.id, 'host_name': host.resource.name}
-
- hosts_initial.append(host_initial)
+ hosts_initial.append({
+ 'host_id': host.id,
+ 'host_name': host.resource.name
+ })
HostFormset = formset_factory(HostSoftwareDefinitionForm, extra=0)
- host_formset = HostFormset(initial=hosts_initial)
+ filter_data = self.build_filter_data(hosts_initial)
- filter_data = {}
- user = self.repo_get(self.repo.SESSION_USER)
- i = 0
- for host_data in hosts_initial:
- host_profile = None
- try:
- host = GenericHost.objects.get(pk=host_data['host_id'])
- host_profile = host.profile
- except Exception:
- for host in hostlist:
- if host.resource.name == host_data['host_name']:
- host_profile = host.profile
- break
- excluded_images = Image.objects.exclude(owner=user).exclude(public=True)
- excluded_images = excluded_images | Image.objects.exclude(host_type=host_profile)
- lab = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE).lab
- excluded_images = excluded_images | Image.objects.exclude(from_lab=lab)
- filter_data["id_form-" + str(i) + "-image"] = []
- for image in excluded_images:
- filter_data["id_form-" + str(i) + "-image"].append(image.name)
- i += 1
+ class SpecialHostFormset(HostFormset):
+ def get_form_kwargs(self, index):
+ kwargs = super(SpecialHostFormset, self).get_form_kwargs(index)
+ if index is not None:
+ kwargs['imageQS'] = Image.objects.exclude(pk__in=filter_data[index])
+ return kwargs
- return host_formset, filter_data
+ if data:
+ return SpecialHostFormset(data, initial=hosts_initial)
+ return SpecialHostFormset(initial=hosts_initial)
def get_host_list(self, grb=None):
if grb is None:
@@ -99,9 +108,9 @@ class Define_Software(WorkflowStep):
if grb:
context["grb"] = grb
- formset, filter_data = self.create_hostformset(self.get_host_list(grb))
+ formset = self.create_hostformset(self.get_host_list(grb))
context["formset"] = formset
- context["filter_data"] = filter_data
+ context['headnode'] = self.repo_get(self.repo.CONFIG_MODELS, {}).get("headnode_index", 1)
else:
context["error"] = "Please select a resource first"
self.metastep.set_invalid("Step requires information that is not yet provided by previous step")
@@ -115,47 +124,35 @@ class Define_Software(WorkflowStep):
confirm = self.repo_get(self.repo.CONFIRMATION, {})
- HostFormset = formset_factory(HostSoftwareDefinitionForm, extra=0)
- formset = HostFormset(request.POST)
hosts = self.get_host_list()
- has_jumphost = False
+ models['headnode_index'] = request.POST.get("headnode", 1)
+ formset = self.create_hostformset(hosts, data=request.POST)
+ has_headnode = False
if formset.is_valid():
models['host_configs'] = []
- i = 0
confirm_hosts = []
- for form in formset:
+ for i, form in enumerate(formset):
host = hosts[i]
- i += 1
image = form.cleaned_data['image']
- # checks image compatability
- grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE)
- lab = None
- if grb:
- lab = grb.lab
- try:
- owner = self.repo_get(self.repo.SESSION_USER)
- q = Image.objects.filter(owner=owner) | Image.objects.filter(public=True)
- q.filter(host_type=host.profile)
- q.filter(from_lab=lab)
- q.get(id=image.id) # will throw exception if image is not in q
- except Exception:
- self.metastep.set_invalid("Image " + image.name + " is not compatible with host " + host.resource.name)
- role = form.cleaned_data['role']
- if "jumphost" in role.name.lower():
- has_jumphost = True
+ headnode = form.cleaned_data['headnode']
+ if headnode:
+ has_headnode = True
bundle = models['bundle']
hostConfig = HostConfiguration(
host=host,
image=image,
bundle=bundle,
- opnfvRole=role
+ is_head_node=headnode
)
models['host_configs'].append(hostConfig)
- confirm_host = {"name": host.resource.name, "image": image.name, "role": role.name}
- confirm_hosts.append(confirm_host)
-
- if not has_jumphost:
- self.metastep.set_invalid('Must have at least one "Jumphost" per POD')
+ confirm_hosts.append({
+ "name": host.resource.name,
+ "image": image.name,
+ "headnode": headnode
+ })
+
+ if not has_headnode:
+ self.metastep.set_invalid('Must have one "Headnode" per POD')
return self.render(request)
self.repo_put(self.repo.CONFIG_MODELS, models)
@@ -172,8 +169,6 @@ class Define_Software(WorkflowStep):
class Config_Software(WorkflowStep):
template = 'config_bundle/steps/config_software.html'
- form = SoftwareConfigurationForm
- context = {'workspace_form': form}
title = "Other Info"
description = "Give your software config a name, description, and other stuff"
short_title = "config info"
@@ -187,58 +182,30 @@ class Config_Software(WorkflowStep):
if bundle:
initial['name'] = bundle.name
initial['description'] = bundle.description
- opnfv = models.get("opnfv", False)
- if opnfv:
- initial['installer'] = opnfv.installer
- initial['scenario'] = opnfv.scenario
- else:
- initial['opnfv'] = False
- supported = {}
- for installer in Installer.objects.all():
- supported[str(installer)] = []
- for scenario in installer.sup_scenarios.all():
- supported[str(installer)].append(str(scenario))
-
- context["form"] = SoftwareConfigurationForm(initial=initial)
- context['supported'] = supported
-
+ context["form"] = BasicMetaForm(initial=initial)
return context
def post_render(self, request):
- try:
- models = self.repo_get(self.repo.CONFIG_MODELS, {})
- if "bundle" not in models:
- models['bundle'] = ConfigBundle(owner=self.repo_get(self.repo.SESSION_USER))
+ models = self.repo_get(self.repo.CONFIG_MODELS, {})
+ if "bundle" not in models:
+ models['bundle'] = ConfigBundle(owner=self.repo_get(self.repo.SESSION_USER))
- confirm = self.repo_get(self.repo.CONFIRMATION, {})
- if "configuration" not in confirm:
- confirm['configuration'] = {}
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ if "configuration" not in confirm:
+ confirm['configuration'] = {}
- form = self.form(request.POST)
- if form.is_valid():
- models['bundle'].name = form.cleaned_data['name']
- models['bundle'].description = form.cleaned_data['description']
- if form.cleaned_data['opnfv']:
- installer = form.cleaned_data['installer']
- scenario = form.cleaned_data['scenario']
- opnfv = OPNFVConfig(
- bundle=models['bundle'],
- installer=installer,
- scenario=scenario
- )
- models['opnfv'] = opnfv
- confirm['configuration']['installer'] = form.cleaned_data['installer'].name
- confirm['configuration']['scenario'] = form.cleaned_data['scenario'].name
-
- confirm['configuration']['name'] = form.cleaned_data['name']
- confirm['configuration']['description'] = form.cleaned_data['description']
- self.metastep.set_valid("Complete")
- else:
- self.metastep.set_invalid("Please correct the errors shown below")
+ form = BasicMetaForm(request.POST)
+ if form.is_valid():
+ models['bundle'].name = form.cleaned_data['name']
+ models['bundle'].description = form.cleaned_data['description']
- self.repo_put(self.repo.CONFIG_MODELS, models)
- self.repo_put(self.repo.CONFIRMATION, confirm)
+ confirm['configuration']['name'] = form.cleaned_data['name']
+ confirm['configuration']['description'] = form.cleaned_data['description']
+ self.metastep.set_valid("Complete")
+ else:
+ self.metastep.set_invalid("Please correct the errors shown below")
+
+ self.repo_put(self.repo.CONFIG_MODELS, models)
+ self.repo_put(self.repo.CONFIRMATION, confirm)
- except Exception:
- pass
return self.render(request)
diff --git a/dashboard/src/workflow/workflow_factory.py b/dashboard/src/workflow/workflow_factory.py
index f5e2ad1..db2bba1 100644
--- a/dashboard/src/workflow/workflow_factory.py
+++ b/dashboard/src/workflow/workflow_factory.py
@@ -12,6 +12,7 @@ from workflow.booking_workflow import Booking_Resource_Select, SWConfig_Select,
from workflow.resource_bundle_workflow import Define_Hardware, Define_Nets, Resource_Meta_Info
from workflow.sw_bundle_workflow import Config_Software, Define_Software, SWConf_Resource_Select
from workflow.snapshot_workflow import Select_Host_Step, Image_Meta_Step
+from workflow.opnfv_workflow import Pick_Installer, Assign_Network_Roles, Assign_Host_Roles, OPNFV_Resource_Select, MetaInfo
from workflow.models import Confirmation_Step
import uuid
@@ -36,6 +37,11 @@ class ConfigMetaWorkflow(object):
color = "#00ffcc"
+class OPNFVMetaWorkflow(object):
+ workflow_type = 3
+ color = "000000"
+
+
class MetaStep(object):
UNTOUCHED = 0
@@ -110,12 +116,21 @@ class WorkflowFactory():
Image_Meta_Step,
]
+ opnfv_steps = [
+ OPNFV_Resource_Select,
+ Pick_Installer,
+ Assign_Network_Roles,
+ Assign_Host_Roles,
+ MetaInfo
+ ]
+
def conjure(self, workflow_type=None, repo=None):
workflow_types = [
self.booking_steps,
self.resource_steps,
self.config_steps,
self.snapshot_steps,
+ self.opnfv_steps,
]
steps = self.make_steps(workflow_types[workflow_type], repository=repo)