OverHaul the Snapshot Workflow
authorParker Berberian <pberberian@iol.unh.edu>
Thu, 17 Jan 2019 17:51:04 +0000 (12:51 -0500)
committerParker Berberian <pberberian@iol.unh.edu>
Fri, 18 Jan 2019 21:08:27 +0000 (16:08 -0500)
Makes the Snapshot workflow much prettier and more functional.

Change-Id: Icdd66f64e6d336ad49ed3cf638a301d0ca92fda9
Signed-off-by: Parker Berberian <pberberian@iol.unh.edu>
src/api/migrations/0004_snapshotconfig_snapshotrelation.py [new file with mode: 0644]
src/api/migrations/0005_snapshotconfig_delta.py [new file with mode: 0644]
src/api/models.py
src/notifier/manager.py
src/templates/notifier/email_fulfilled.txt
src/templates/snapshot_workflow/steps/meta.html
src/templates/snapshot_workflow/steps/select_host.html
src/workflow/forms.py
src/workflow/models.py
src/workflow/snapshot_workflow.py

diff --git a/src/api/migrations/0004_snapshotconfig_snapshotrelation.py b/src/api/migrations/0004_snapshotconfig_snapshotrelation.py
new file mode 100644 (file)
index 0000000..62bc7af
--- /dev/null
@@ -0,0 +1,42 @@
+# Generated by Django 2.1 on 2019-01-17 15:54
+
+import api.models
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('resource_inventory', '0004_auto_20181017_1532'),
+        ('api', '0003_auto_20190102_1956'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SnapshotConfig',
+            fields=[
+                ('taskconfig_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='api.TaskConfig')),
+                ('image', models.IntegerField(null=True)),
+                ('dashboard_id', models.IntegerField()),
+                ('host', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='resource_inventory.Host')),
+            ],
+            bases=('api.taskconfig',),
+        ),
+        migrations.CreateModel(
+            name='SnapshotRelation',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('status', models.IntegerField(default=0)),
+                ('task_id', models.CharField(default=api.models.get_task_uuid, max_length=37)),
+                ('lab_token', models.CharField(default='null', max_length=50)),
+                ('message', models.TextField(default='')),
+                ('config', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.SnapshotConfig')),
+                ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Job')),
+                ('snapshot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Image')),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
+    ]
diff --git a/src/api/migrations/0005_snapshotconfig_delta.py b/src/api/migrations/0005_snapshotconfig_delta.py
new file mode 100644 (file)
index 0000000..559af90
--- /dev/null
@@ -0,0 +1,18 @@
+# Generated by Django 2.1 on 2019-01-17 16:07
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('api', '0004_snapshotconfig_snapshotrelation'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='snapshotconfig',
+            name='delta',
+            field=models.TextField(default='{}'),
+        ),
+    ]
index a1fedfe..e4016aa 100644 (file)
@@ -214,6 +214,10 @@ class Job(models.Model):
             if 'network' not in d:
                 d['network'] = {}
             d['network'][relation.task_id] = relation.config.to_dict()
+        for relation in SnapshotRelation.objects.filter(job=self):
+            if 'snapshot' not in d:
+                d['snapshot'] = {}
+            d['snapshot'][relation.task_id] = relation.config.to_dict()
 
         j['payload'] = d
 
@@ -221,7 +225,13 @@ class Job(models.Model):
 
     def get_tasklist(self, status="all"):
         tasklist = []
-        clist = [HostHardwareRelation, AccessRelation, HostNetworkRelation, SoftwareRelation]
+        clist = [
+            HostHardwareRelation,
+            AccessRelation,
+            HostNetworkRelation,
+            SoftwareRelation,
+            SnapshotRelation
+        ]
         if status == "all":
             for cls in clist:
                 tasklist += list(cls.objects.filter(job=self))
@@ -261,6 +271,10 @@ class Job(models.Model):
             if 'network' not in d:
                 d['network'] = {}
             d['network'][relation.task_id] = relation.config.get_delta()
+        for relation in SnapshotRelation.objects.filter(job=self).filter(status=status):
+            if 'snapshot' not in d:
+                d['snapshot'] = {}
+            d['snapshot'][relation.task_id] = relation.config.get_delta()
 
         j['payload'] = d
         return j
@@ -534,6 +548,61 @@ class NetworkConfig(TaskConfig):
         self.delta = json.dumps(d)
 
 
+class SnapshotConfig(TaskConfig):
+
+    host = models.ForeignKey(Host, null=True, on_delete=models.DO_NOTHING)
+    image = models.IntegerField(null=True)
+    dashboard_id = models.IntegerField()
+    delta = models.TextField(default="{}")
+
+    def to_dict(self):
+        d = {}
+        if self.host:
+            d['host'] = self.host.labid
+        if self.image:
+            d['image'] = self.image
+        d['dashboard_id'] = self.dashboard_id
+        return d
+
+    def to_json(self):
+        return json.dumps(self.to_dict())
+
+    def get_delta(self):
+        if not self.delta:
+            self.delta = self.to_json()
+            self.save()
+        d = json.loads(self.delta)
+        return d
+
+    def clear_delta(self):
+        self.delta = json.dumps(self.to_dict())
+        self.save()
+
+    def set_host(self, host):
+        self.host = host
+        d = json.loads(self.delta)
+        d['host'] = host.labid
+        self.delta = json.dumps(d)
+
+    def set_image(self, image):
+        self.image = image
+        d = json.loads(self.delta)
+        d['image'] = self.image
+        self.delta = json.dumps(d)
+
+    def clear_image(self):
+        self.image = None
+        d = json.loads(self.delta)
+        d.pop("image", None)
+        self.delta = json.dumps(d)
+
+    def set_dashboard_id(self, dash):
+        self.dashboard_id = dash
+        d = json.loads(self.delta)
+        d['dashboard_id'] = self.dashboard_id
+        self.delta = json.dumps(d)
+
+
 def get_task(task_id):
     for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation]:
         try:
@@ -617,8 +686,40 @@ class HostNetworkRelation(TaskRelation):
         return super(self.__class__, self).delete(*args, **kwargs)
 
 
+class SnapshotRelation(TaskRelation):
+    snapshot = models.ForeignKey(Image, on_delete=models.CASCADE)
+    config = models.OneToOneField(SnapshotConfig, on_delete=models.CASCADE)
+
+    def type_str(self):
+        return "Snapshot Task"
+
+    def get_delta(self):
+        return self.config.to_dict()
+
+    def delete(self, *args, **kwargs):
+        self.config.delete()
+        return super(self.__class__, self).delete(*args, **kwargs)
+
+
 class JobFactory(object):
 
+    @classmethod
+    def makeSnapshotTask(cls, image, booking, host):
+        relation = SnapshotRelation()
+        job = Job.objects.get(booking=booking)
+        config = SnapshotConfig.objects.create(dashboard_id=image.id)
+
+        relation.job = job
+        relation.config = config
+        relation.config.save()
+        relation.config = relation.config
+        relation.snapshot = image
+        relation.save()
+
+        config.clear_delta()
+        config.set_host(host)
+        config.save()
+
     @classmethod
     def makeCompleteJob(cls, booking):
         hosts = Host.objects.filter(bundle=booking.resource)
index 3361074..f03c2cc 100644 (file)
@@ -75,7 +75,7 @@ class NotificationHandler(object):
                 if (not hasattr(task, "user")) or task.user == user:
                     user_tasklist.append(
                         {
-                            "title": task.type_str + " Message: ",
+                            "title": task.type_str() + " Message: ",
                             "content": task.message
                         }
                     )
@@ -94,7 +94,7 @@ class NotificationHandler(object):
                 "Your Booking is Ready",
                 message,
                 os.environ.get("DEFAULT_FROM_EMAIL", "opnfv@pharos-dashboard"),
-                user.userprofile.email_addr,
+                [user.userprofile.email_addr],
                 fail_silently=False
             )
 
index d473961..65593db 100644 (file)
@@ -3,9 +3,9 @@
 The booking you requested of the OPNFV Lab as a Service has finished deploying and is ready for you to use.
 
 The lab that fulfilled your booking request has sent you the following messages:
-    {% for message in messages %}
-        {% message.title %}
-        {% message.content %}
+    {% for email_message in messages %}
+        {{ email_message.title }}
+        {{ email_message.content }}
         --------------------
     {% endfor %}
 
index 2e767cc..cc49691 100644 (file)
@@ -4,16 +4,21 @@
 {% load bootstrap3 %}
 
 {% block content %}
-
-
 <style>
+.meta_container {
+    padding: 50px;
+}
 </style>
-
 {% bootstrap_form_errors form type='non_fields' %}
-<form id="meta_form" action="/wf/workflow/" method="POST" class="form">
-{% csrf_token %}
-{{form}}
-</form>
+<div class="meta_container">
+    <form id="meta_form" action="/wf/workflow/" method="POST" class="form">
+    {% csrf_token %}
+    <div class="form-group">
+    {% bootstrap_field form.name %}
+    {% bootstrap_field form.description %}
+    </div>
+    </form>
+</div>
 {% endblock content %}
 
 {% block onleave %}
index 16dd5d4..27a9238 100644 (file)
@@ -5,43 +5,73 @@
 
 {% block content %}
 
-
 <style>
     .booking {
-        border-style: solid;
+        border-style: none;
         border-color: black;
-        border-width: 2px;
-        display: inline-block;
-        padding: 3px;
+        border: 2px;
+        border-radius: 5px;
+        margin: 20px;
+        padding-left: 25px;
+        padding-right: 25px;
+        padding-bottom: 25px;
+        box-shadow: 0px 0px 7px 0px rgba(0,0,0,0.75);
+        transition-property: box-shadow;
+        transition-duration: 0.1s;
+        float: left;
+    }
+    .booking:hover {
+        box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.75);
+        transition-property: box-shadow;
+        transition-duration: 0.1s;
     }
     .host {
+        cursor: pointer;
         border-style: solid;
         border-color: black;
         border-width: 1px;
-        margin: 2px;
+        border-radius: 5px;
+        margin: 5px;
+        padding: 5px;
+        text-align: center;
+        box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.75);
+        transition-property: box-shadow;
+        transition-duration: 0.1s;
+    }
+    .host:hover {
+        box-shadow: 0px 0px 4px 0px rgba(0,0,0,0.75);
+        transition-property: box-shadow;
+        transition-duration: 0.1s;
+        background-color: rgba(144,238,144,0.3);
+    }
+    .selected {
+        background-color: lightgreen !important;
+    }
+    .booking_container {
+        overflow: auto;
+        padding: 30px;
     }
 </style>
-
 {% bootstrap_form_errors form type='non_fields' %}
 <form id="host_select_form" action="/wf/workflow/" method="POST" class="form">
 {% csrf_token %}
 <input type="hidden" id="hidden_json_input", name="host"/>
 </form>
-<div id="host_select_container">
+<div id="host_select_container" class="booking_container">
 </div>
 <script>
 var selected_host = null;
-var initial = {{chosen|default:'null'}};
+var initial = {{chosen|safe|default:'null'}};
 
 function select(booking_id, host_name){
     var input = document.getElementById("hidden_json_input");
     input.value = JSON.stringify({"booking": booking_id, "name": host_name});
     // clear out and highlist host
     if(selected_host){
-        selected_host.style['background-color'] = "white";
+        selected_host.classList.remove("selected");
     }
     selected_host = document.getElementById("booking_" + booking_id + "_host_" + host_name);
-    selected_host.style['background-color'] = "lightgrey";
+    selected_host.classList.add("selected");
 }
 
 function draw_bookings(){
@@ -53,17 +83,20 @@ function draw_bookings(){
         var heading = document.createElement("H3");
         heading.appendChild(document.createTextNode("Booking " + booking_id));
         booking.appendChild(heading);
-        var desc = "start: " + booking_hosts[booking_id].start +
-        " end: " + booking_hosts[booking_id].end +
-        " purpose: " + booking_hosts[booking_id].purpose;
-        booking.appendChild(document.createTextNode(desc));
+        booking.appendChild(document.createTextNode("start: " + booking_hosts[booking_id].start));
+        booking.appendChild(document.createElement("BR"));
+        booking.appendChild(document.createTextNode("end: " + booking_hosts[booking_id].end));
+        booking.appendChild(document.createElement("BR"));
+        booking.appendChild(document.createTextNode("purpose: " + booking_hosts[booking_id].purpose));
+        booking.appendChild(document.createElement("BR"));
+        booking.appendChild(document.createTextNode("hosts:"));
         booking.id = "booking_" + booking_id;
         booking.className = "booking";
         var hosts = booking_hosts[booking_id].hosts;
         for(var i=0; i<hosts.length; i++){
             var host = document.createElement("DIV");
             host.id = "booking_" + booking_id + "_host_" + hosts[i].name;
-            host.className = "host";
+            host.classList.add("host");
             host.appendChild(document.createTextNode(hosts[i].name));
             var hostname = hosts[i].name;
             host.booking = booking_id;
index b8c7f66..726e7dd 100644 (file)
@@ -461,7 +461,7 @@ class SnapshotHostSelectForm(forms.Form):
 
 class SnapshotMetaForm(forms.Form):
     name = forms.CharField()
-    description = forms.CharField()
+    description = forms.CharField(widget=forms.Textarea)
 
 
 class ConfirmationForm(forms.Form):
index 495ce07..4e79546 100644 (file)
@@ -11,6 +11,7 @@
 from django.shortcuts import render
 from django.contrib import messages
 from django.http import HttpResponse
+from django.utils import timezone
 
 import yaml
 import requests
@@ -385,6 +386,8 @@ class Repository():
         if not booking_id:
             return "SNAP, No booking ID provided"
         booking = Booking.objects.get(pk=booking_id)
+        if booking.start > timezone.now() or booking.end < timezone.now():
+            return "Booking is not active"
         name = self.el.get(self.SNAPSHOT_NAME)
         if not name:
             return "SNAP, no name provided"
@@ -400,6 +403,13 @@ class Repository():
         image.owner = owner
         image.host_type = host.profile
         image.save()
+        try:
+            current_image = host.config.image
+            image.os = current_image.os
+            image.save()
+        except Exception:
+            pass
+        JobFactory.makeSnapshotTask(image, booking, host)
 
     def make_generic_resource_bundle(self):
         owner = self.el[self.SESSION_USER]
index 4ddc397..0d53ed4 100644 (file)
@@ -87,7 +87,14 @@ class Image_Meta_Step(WorkflowStep):
 
     def get_context(self):
         context = super(Image_Meta_Step, self).get_context()
-        context['form'] = SnapshotMetaForm()
+        name = self.repo_get(self.repo.SNAPSHOT_NAME, False)
+        desc = self.repo_get(self.repo.SNAPSHOT_DESC, False)
+        form = None
+        if name and desc:
+            form = SnapshotMetaForm(initial={"name": name, "description": desc})
+        else:
+            form = SnapshotMetaForm()
+        context['form'] = form
         return context
 
     def post_render(self, request):