Implement OPNFV workflow 13/67513/17
authorSawyer Bergeron <sbergeron@iol.unh.edu>
Tue, 9 Apr 2019 20:30:57 +0000 (16:30 -0400)
committerParker Berberian <pberberian@iol.unh.edu>
Fri, 3 May 2019 15:48:22 +0000 (11:48 -0400)
This is a counterpart to an update to network models,
and allows for configuring baremetal OPNFV and Openstack deploys

Change-Id: I0185dbfa6c9105d7e63a7e7d7dd1f5cf228a8877
Signed-off-by: Sawyer Bergeron <sbergeron@iol.unh.edu>
Signed-off-by: Parker Berberian <pberberian@iol.unh.edu>
24 files changed:
dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py [new file with mode: 0644]
dashboard/src/api/models.py
dashboard/src/booking/migrations/0006_booking_opnfv_config.py [new file with mode: 0644]
dashboard/src/booking/models.py
dashboard/src/booking/quick_deployer.py
dashboard/src/resource_inventory/idf_templater.py
dashboard/src/resource_inventory/migrations/0010_auto_20190430_1405.py [new file with mode: 0644]
dashboard/src/resource_inventory/models.py
dashboard/src/resource_inventory/pdf_templater.py
dashboard/src/templates/base.html
dashboard/src/templates/booking/booking_table.html
dashboard/src/templates/config_bundle/steps/assign_host_roles.html [new file with mode: 0644]
dashboard/src/templates/config_bundle/steps/assign_network_roles.html [new file with mode: 0644]
dashboard/src/templates/config_bundle/steps/config_software.html
dashboard/src/templates/config_bundle/steps/define_software.html
dashboard/src/templates/config_bundle/steps/pick_installer.html [new file with mode: 0644]
dashboard/src/templates/config_bundle/steps/table_formset.html [new file with mode: 0644]
dashboard/src/templates/resource/steps/meta_info.html
dashboard/src/workflow/forms.py
dashboard/src/workflow/models.py
dashboard/src/workflow/opnfv_workflow.py [new file with mode: 0644]
dashboard/src/workflow/snapshot_workflow.py
dashboard/src/workflow/sw_bundle_workflow.py
dashboard/src/workflow/workflow_factory.py

diff --git a/dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py b/dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py
new file mode 100644 (file)
index 0000000..46f3631
--- /dev/null
@@ -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'),
+        ),
+    ]
index 4ce8c3e..e17a911 100644 (file)
@@ -23,7 +23,8 @@ from resource_inventory.models import (
     Host,
     Image,
     Interface,
-    RemoteInfo
+    RemoteInfo,
+    OPNFVConfig
 )
 from resource_inventory.idf_templater import IDFTemplater
 from resource_inventory.pdf_templater import PDFTemplater
@@ -93,7 +94,7 @@ class LabManager(object):
         return {"status": "success"}
 
     def update_xdf(self, booking):
-        booking.pdf = PDFTemplater.makePDF(booking.resource)
+        booking.pdf = PDFTemplater.makePDF(booking)
         booking.idf = IDFTemplater().makeIDF(booking)
         booking.save()
 
@@ -356,9 +357,12 @@ class OpnfvApiConfig(models.Model):
     scenario = models.CharField(max_length=300)
     roles = models.ManyToManyField(Host)
     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:
@@ -367,8 +371,12 @@ class OpnfvApiConfig(models.Model):
         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
 
@@ -818,6 +826,7 @@ class JobFactory(object):
         )
         cls.makeSoftware(
             hosts=hosts,
+            booking=booking,
             job=job
         )
         all_users = list(booking.collaborators.all())
@@ -908,28 +917,19 @@ 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
-
-        try:
-            host = None
-            if len(hosts) > 0:
-                host = hosts[0]
-            opnfv_config = init_config(host)
+    def makeSoftware(cls, hosts=[], booking=None, job=Job()):
 
-            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
-        except Exception:
+        if not booking.opnfv_config:
             return None
+
+        opnfv_api_config = OpnfvApiConfig.objects.create(
+            opnfv_config=booking.opnfv_config,
+            installer=booking.opnfv_config.installer,
+            scenario=booking.opnfv_config.scenario,
+        )
+
+        for host in hosts:
+            opnfv_api_config.roles.add(host)
+        software_config = SoftwareConfig.objects.create(opnfv=opnfv_api_config)
+        software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
+        return software_relation
diff --git a/dashboard/src/booking/migrations/0006_booking_opnfv_config.py b/dashboard/src/booking/migrations/0006_booking_opnfv_config.py
new file mode 100644 (file)
index 0000000..e5ffc71
--- /dev/null
@@ -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'),
+        ),
+    ]
index 02e03cc..9836730 100644 (file)
@@ -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,6 +29,7 @@ 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="")
index 640ded9..763c8a0 100644 (file)
@@ -32,7 +32,8 @@ from resource_inventory.models import (
     OPNFVConfig,
     Network,
     NetworkConnection,
-    NetworkRole
+    NetworkRole,
+    HostOPNFVConfig,
 )
 from resource_inventory.resource_manager import ResourceManager
 from resource_inventory.pdf_templater import PDFTemplater
@@ -185,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()
@@ -273,18 +286,16 @@ def create_from_form(form, request):
     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():
@@ -297,24 +308,27 @@ def create_from_form(form, request):
     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)
index 7cd13bb..26307e3 100644 (file)
@@ -12,10 +12,7 @@ from django.template.loader import render_to_string
 
 from account.models import PublicNetwork
 
-from resource_inventory.models import (
-    OPNFVConfig,
-    Vlan
-)
+from resource_inventory.models import Vlan
 
 
 class IDFTemplater:
@@ -67,7 +64,7 @@ class IDFTemplater:
 
     def get_public_net(self, booking):
         public = {}
-        config = OPNFVConfig.objects.get(bundle=booking.config_bundle)
+        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)
@@ -91,7 +88,7 @@ class IDFTemplater:
         return net
 
     def get_single_net_config(self, booking, net_name):
-        config = OPNFVConfig.objects.get(bundle=booking.config_bundle)
+        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
@@ -127,7 +124,10 @@ class IDFTemplater:
         return bridges
 
     def get_fuel_nodes(self, booking):
-        hosts = booking.resource.hosts.exclude(config__opnfvRole__name="jumphost")
+        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 = {}
diff --git a/dashboard/src/resource_inventory/migrations/0010_auto_20190430_1405.py b/dashboard/src/resource_inventory/migrations/0010_auto_20190430_1405.py
new file mode 100644 (file)
index 0000000..3823eaf
--- /dev/null
@@ -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'),
+        ),
+    ]
index d3f47d4..b9f2c44 100644 (file)
@@ -191,7 +191,7 @@ 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()
+        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):
@@ -252,6 +252,8 @@ class OPNFVConfig(models.Model):
     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)
@@ -297,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)
@@ -353,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"]
index 2db2129..d08b303 100644 (file)
@@ -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,22 +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)
         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))
 
index 02c67dc..f48a201 100644 (file)
                                     <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>
index e0c5f49..32a0146 100644 (file)
@@ -30,7 +30,7 @@
             {{ booking.end }}
         </td>
         <td>
-            {{ booking.resource.get_host.config.image.os.name }}
+            {{ booking.resource.get_head_node.config.image.os.name }}
         </td>
     </tr>
 {% endfor %}
diff --git a/dashboard/src/templates/config_bundle/steps/assign_host_roles.html b/dashboard/src/templates/config_bundle/steps/assign_host_roles.html
new file mode 100644 (file)
index 0000000..3ba7665
--- /dev/null
@@ -0,0 +1,22 @@
+{% extends "config_bundle/steps/table_formset.html" %}
+
+{% load bootstrap3 %}
+
+{% block table %}
+<thead>
+    <tr>
+        <th>Host</th>
+        <th>Role</th>
+    </tr>
+</thead>
+<tbody>
+    {% for form in formset %}
+    <tr>
+        <td>{% bootstrap_field form.host_name show_label=False %}</td>
+        <td>{% bootstrap_field form.role  show_label=False %}</td>
+    </tr>
+    {% endfor %}
+</tbody>
+
+{{formset.management_form}}
+{% endblock table %}
diff --git a/dashboard/src/templates/config_bundle/steps/assign_network_roles.html b/dashboard/src/templates/config_bundle/steps/assign_network_roles.html
new file mode 100644 (file)
index 0000000..0e887d6
--- /dev/null
@@ -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 %}
index e1f9541..b181c7e 100644 (file)
@@ -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 %}
 
index 8e7be91..ba1ff34 100644 (file)
-{% extends "workflow/viewport-element.html" %}
-{% load staticfiles %}
+{% extends "config_bundle/steps/table_formset.html" %}
 
 {% load bootstrap3 %}
 
+{% block table %}
+    <thead>
+        <tr>
+            <th>Device</th>
+            <th>Image</th>
+            <th>HeadNode</th>
+        </tr>
+    </thead>
+    <tbody>
+{% for form in formset %}
+    <tr>
+        <td>{% bootstrap_field form.host_name show_label=False %}</td>
+        <td>{% bootstrap_field form.image show_label=False %}</td>
+        <td class="table_hidden_input_parent">
+            <input id="radio_{{forloop.counter}}" class="my_radio" type="radio" name="headnode" value="{{forloop.counter}}">
+            {{ form.headnode }}
+        </td>
+    </tr>
+{% endfor %}
+{{formset.management_form}}
+
+{% endblock table %}
+
+{% block tablejs %}
+<script>
+
+    document.getElementById("radio_{{headnode}}").checked = true;
+
+</script>
+{% endblock tablejs %}
 
-{% block extrahead %}
-    <!-- DataTables CSS -->
-    <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
-          rel="stylesheet">
 
-    <!-- DataTables Responsive CSS -->
-    <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}" rel="stylesheet">
-{% endblock extrahead %}
-
-{% block content %}
-{% if error %}
-    <h1 style="text-align:center;">{{ error }}</h1>
-{% else %}
-    <form style="width: 90%; margin: 5%;" method="post" action="" class="form" id="softwaredefinitionform">
-        {% csrf_token %}
-
-    <div class="row">
-        <div class="col-lg-12">
-                    <div class="dataTables_wrapper">
-                        <table class="table table-striped table-bordered table-hover" id="table" cellspacing="0"
-                               width="100%">
-
-                            {% block table %}
-                                <thead>
-                                    <tr>
-                                        <th>Device</th>
-                                        <th>Role</th>
-                                        <th>Image</th>
-                                    </tr>
-                                </thead>
-                                <tbody>
-                            {% for form in formset %}
-                                <tr>
-                                {% for field in form %}
-                                    <td>{{ field }}</td>
-                                {% endfor %}
-                                </tr>
-                            {% endfor %}
-                            {{formset.management_form}}
-
-                            {% endblock table %}
-
-                        </table>
-                    </div>
-                    <!-- /.table-responsive -->
-                <!-- /.panel-body -->
-            <!-- /.panel -->
-        </div>
-        <!-- /.col-lg-12 -->
-    </div>
-    </form>
-
-    <script>
-function filter_images(){
-    var filter_data = {{filter_data|safe}};
-    for(var key in filter_data){
-        var dropdown = document.getElementById(key);
-        var to_remove = filter_data[key];
-        for(var i=0; i<to_remove.length; i++){
-            for(var j=dropdown.children.length-1; j>=0; j--){
-                if(dropdown.children[j].text == to_remove[i]){
-                    dropdown.removeChild(dropdown.children[j]);
-                }
-            }
-        }
+{% block onleave %}
+var parents = document.getElementsByClassName("table_hidden_input_parent");
+for(var i=0; i<parents.length; i++){
+    var node = parents[i];
+    var radio = node.getElementsByClassName("my_radio")[0];
+    var checkbox = radio.nextElementSibling;
+    if(radio.checked){
+        checkbox.value = "True";
     }
 }
 
-filter_images();
-    </script>
-{% endif %}
-{% endblock content %}
-
-{% block extrajs %}
-    {{ block.super }}
-    <!-- DataTables JavaScript -->
-
-    <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
-    <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
-
-    <script src={% static "js/dataTables-sort.js" %}></script>
-
-    {% block tablejs %}
-    {% endblock tablejs %}
-{% endblock extrajs %}
-
-
-{% block onleave %}
-var form = $("#softwaredefinitionform");
+var form = $("#table_formset");
 var formData = form.serialize();
 var req = new XMLHttpRequest();
 req.open("POST", "/wf/workflow/", false);
 req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
 req.onerror = function() { alert("problem with form submission"); }
 req.send(formData);
-{% endblock %}
+{% endblock onleave %}
diff --git a/dashboard/src/templates/config_bundle/steps/pick_installer.html b/dashboard/src/templates/config_bundle/steps/pick_installer.html
new file mode 100644 (file)
index 0000000..3b170d9
--- /dev/null
@@ -0,0 +1,32 @@
+{% extends "workflow/viewport-element.html" %}
+{% load staticfiles %}
+
+{% load bootstrap3 %}
+
+{% block content %}
+
+{% if unavailable %}
+<h1>Please choose a config bundle first</h1>
+{% else %}
+
+<form id="installer_form" action="/wf/workflow/" method="POST" id="installer_config_form" class="form">
+    {% csrf_token %}
+    <p>Choose your installer:</p>
+    {% bootstrap_field form.installer %}
+    <p>Choose your scenario:</p>
+    {% bootstrap_field form.scenario %}
+</form>
+
+{% endif %}
+
+{% endblock content %}
+
+{% block onleave %}
+var form = $("#installer_form");
+var formData = form.serialize();
+var req = new XMLHttpRequest();
+req.open("POST", "/wf/workflow/", false);
+req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+req.onerror = function() { alert("problem with form submission"); }
+req.send(formData);
+{% endblock %}
diff --git a/dashboard/src/templates/config_bundle/steps/table_formset.html b/dashboard/src/templates/config_bundle/steps/table_formset.html
new file mode 100644 (file)
index 0000000..ad2c5a3
--- /dev/null
@@ -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 %}
index 7a1b56a..da98267 100644 (file)
@@ -7,7 +7,7 @@
 
 <style>
 #resource_meta_form {
-    margin: 80px;
+    padding: 80px;
     display: grid;
 }
 
index b40713f..6d26b5c 100644 (file)
@@ -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"))
index 4ebb042..bf5751d 100644 (file)
@@ -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
@@ -259,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"
@@ -268,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"
@@ -339,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:
@@ -536,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)
@@ -551,6 +561,53 @@ class Repository():
         except Exception as e:
             return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0016"
 
+    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 = {}
         self.el[self.CONFIRMATION] = {}
diff --git a/dashboard/src/workflow/opnfv_workflow.py b/dashboard/src/workflow/opnfv_workflow.py
new file mode 100644 (file)
index 0000000..26e1d7c
--- /dev/null
@@ -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)
index 002aee5..34ac3a5 100644 (file)
@@ -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)
index fd41018..a6a7464 100644 (file)
@@ -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)
index f5e2ad1..db2bba1 100644 (file)
@@ -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)