diff options
46 files changed, 1845 insertions, 729 deletions
diff --git a/src/api/migrations/0007_auto_20190417_1511.py b/src/api/migrations/0007_auto_20190417_1511.py new file mode 100644 index 0000000..e7d2c59 --- /dev/null +++ b/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/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py b/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py new file mode 100644 index 0000000..46f3631 --- /dev/null +++ b/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/src/api/migrations/0008_auto_20190419_1414.py b/src/api/migrations/0008_auto_20190419_1414.py new file mode 100644 index 0000000..03c3865 --- /dev/null +++ b/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/src/api/migrations/0009_merge_20190508_1317.py b/src/api/migrations/0009_merge_20190508_1317.py new file mode 100644 index 0000000..1a34380 --- /dev/null +++ b/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/src/api/models.py b/src/api/models.py index f8b8f89..1f708ae 100644 --- a/src/api/models.py +++ b/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/src/api/urls.py b/src/api/urls.py index d18a04d..d1f772a 100644 --- a/src/api/urls.py +++ b/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/src/api/views.py b/src/api/views.py index 2ae1ac5..fb28958 100644 --- a/src/api/views.py +++ b/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/src/booking/forms.py b/src/booking/forms.py index 7ba5af0..de427ab 100644 --- a/src/booking/forms.py +++ b/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/src/booking/migrations/0005_booking_idf.py b/src/booking/migrations/0005_booking_idf.py new file mode 100644 index 0000000..31e9170 --- /dev/null +++ b/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/src/booking/migrations/0006_booking_opnfv_config.py b/src/booking/migrations/0006_booking_opnfv_config.py new file mode 100644 index 0000000..e5ffc71 --- /dev/null +++ b/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/src/booking/models.py b/src/booking/models.py index 8612abd..9836730 100644 --- a/src/booking/models.py +++ b/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/src/booking/quick_deployer.py b/src/booking/quick_deployer.py index f076a2e..763c8a0 100644 --- a/src/booking/quick_deployer.py +++ b/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/src/dashboard/views.py b/src/dashboard/views.py index c4a6685..aaad7ab 100644 --- a/src/dashboard/views.py +++ b/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/src/resource_inventory/admin.py b/src/resource_inventory/admin.py index e063cc0..7ff510b 100644 --- a/src/resource_inventory/admin.py +++ b/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/src/resource_inventory/idf_templater.py b/src/resource_inventory/idf_templater.py new file mode 100644 index 0000000..bf6eda0 --- /dev/null +++ b/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/src/resource_inventory/migrations/0009_auto_20190315_1757.py b/src/resource_inventory/migrations/0009_auto_20190315_1757.py new file mode 100644 index 0000000..92ed0e9 --- /dev/null +++ b/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/src/resource_inventory/migrations/0010_auto_20190430_1405.py b/src/resource_inventory/migrations/0010_auto_20190430_1405.py new file mode 100644 index 0000000..3823eaf --- /dev/null +++ b/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/src/resource_inventory/models.py b/src/resource_inventory/models.py index bdc1f5d..b9f2c44 100644 --- a/src/resource_inventory/models.py +++ b/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/src/resource_inventory/pdf_templater.py b/src/resource_inventory/pdf_templater.py index a50f04c..2302530 100644 --- a/src/resource_inventory/pdf_templater.py +++ b/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/src/resource_inventory/resource_manager.py b/src/resource_inventory/resource_manager.py index 52b0055..652e4e3 100644 --- a/src/resource_inventory/resource_manager.py +++ b/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/src/resource_inventory/urls.py b/src/resource_inventory/urls.py index 4e159ba..a72871b 100644 --- a/src/resource_inventory/urls.py +++ b/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/src/resource_inventory/views.py b/src/resource_inventory/views.py index 2937bd7..8c3d899 100644 --- a/src/resource_inventory/views.py +++ b/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/src/templates/base.html b/src/templates/base.html index 02c67dc..f48a201 100644 --- a/src/templates/base.html +++ b/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/src/templates/booking/booking_table.html b/src/templates/booking/booking_table.html index e0c5f49..32a0146 100644 --- a/src/templates/booking/booking_table.html +++ b/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/src/templates/booking/quick_deploy.html b/src/templates/booking/quick_deploy.html index 819bf05..38294b2 100644 --- a/src/templates/booking/quick_deploy.html +++ b/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/src/templates/booking/steps/booking_meta.html b/src/templates/booking/steps/booking_meta.html index e4881ae..fe43f53 100644 --- a/src/templates/booking/steps/booking_meta.html +++ b/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/src/templates/config_bundle/steps/assign_host_roles.html b/src/templates/config_bundle/steps/assign_host_roles.html new file mode 100644 index 0000000..3ba7665 --- /dev/null +++ b/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/src/templates/config_bundle/steps/assign_network_roles.html b/src/templates/config_bundle/steps/assign_network_roles.html new file mode 100644 index 0000000..0e887d6 --- /dev/null +++ b/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/src/templates/config_bundle/steps/config_software.html b/src/templates/config_bundle/steps/config_software.html index e1f9541..b181c7e 100644 --- a/src/templates/config_bundle/steps/config_software.html +++ b/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/src/templates/config_bundle/steps/define_software.html b/src/templates/config_bundle/steps/define_software.html index 8e7be91..ba1ff34 100644 --- a/src/templates/config_bundle/steps/define_software.html +++ b/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/src/templates/config_bundle/steps/pick_installer.html b/src/templates/config_bundle/steps/pick_installer.html new file mode 100644 index 0000000..3b170d9 --- /dev/null +++ b/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/src/templates/config_bundle/steps/table_formset.html b/src/templates/config_bundle/steps/table_formset.html new file mode 100644 index 0000000..ad2c5a3 --- /dev/null +++ b/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/src/templates/dashboard/idf.yaml b/src/templates/dashboard/idf.yaml index 5da20c4..9e0cc26 100644 --- a/src/templates/dashboard/idf.yaml +++ b/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/src/templates/dashboard/lab_detail.html b/src/templates/dashboard/lab_detail.html index a30ac9e..7938e86 100644 --- a/src/templates/dashboard/lab_detail.html +++ b/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/src/templates/resource/hostprofile_detail.html b/src/templates/resource/hostprofile_detail.html new file mode 100644 index 0000000..0776b9e --- /dev/null +++ b/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/src/templates/resource/hosts.html b/src/templates/resource/hosts.html index 4bf64e0..69b7231 100644 --- a/src/templates/resource/hosts.html +++ b/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/src/templates/resource/steps/meta_info.html b/src/templates/resource/steps/meta_info.html index 7a1b56a..da98267 100644 --- a/src/templates/resource/steps/meta_info.html +++ b/src/templates/resource/steps/meta_info.html @@ -7,7 +7,7 @@ <style> #resource_meta_form { - margin: 80px; + padding: 80px; display: grid; } diff --git a/src/templates/resource/steps/pod_definition.html b/src/templates/resource/steps/pod_definition.html index 8599bb0..2cb6257 100644 --- a/src/templates/resource/steps/pod_definition.html +++ b/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/src/templates/workflow/viewport-base.html b/src/templates/workflow/viewport-base.html index f78bc01..1329595 100644 --- a/src/templates/workflow/viewport-base.html +++ b/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/src/workflow/forms.py b/src/workflow/forms.py index b40713f..6d26b5c 100644 --- a/src/workflow/forms.py +++ b/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/src/workflow/models.py b/src/workflow/models.py index cdfddef..bf5751d 100644 --- a/src/workflow/models.py +++ b/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/src/workflow/opnfv_workflow.py b/src/workflow/opnfv_workflow.py new file mode 100644 index 0000000..26e1d7c --- /dev/null +++ b/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/src/workflow/resource_bundle_workflow.py b/src/workflow/resource_bundle_workflow.py index 4858ebe..536187f 100644 --- a/src/workflow/resource_bundle_workflow.py +++ b/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('"', '"') - 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('"', '"') - 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('"', '"') + 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('"', '"') + 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/src/workflow/snapshot_workflow.py b/src/workflow/snapshot_workflow.py index 002aee5..34ac3a5 100644 --- a/src/workflow/snapshot_workflow.py +++ b/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/src/workflow/sw_bundle_workflow.py b/src/workflow/sw_bundle_workflow.py index fd41018..a6a7464 100644 --- a/src/workflow/sw_bundle_workflow.py +++ b/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/src/workflow/workflow_factory.py b/src/workflow/workflow_factory.py index f5e2ad1..db2bba1 100644 --- a/src/workflow/workflow_factory.py +++ b/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) |