aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/api/migrations/0004_snapshotconfig_snapshotrelation.py42
-rw-r--r--src/api/migrations/0005_snapshotconfig_delta.py18
-rw-r--r--src/api/models.py103
-rw-r--r--src/notifier/manager.py4
-rw-r--r--src/templates/notifier/email_fulfilled.txt6
-rw-r--r--src/templates/snapshot_workflow/steps/meta.html19
-rw-r--r--src/templates/snapshot_workflow/steps/select_host.html65
-rw-r--r--src/workflow/forms.py2
-rw-r--r--src/workflow/models.py10
-rw-r--r--src/workflow/snapshot_workflow.py9
10 files changed, 247 insertions, 31 deletions
diff --git a/src/api/migrations/0004_snapshotconfig_snapshotrelation.py b/src/api/migrations/0004_snapshotconfig_snapshotrelation.py
new file mode 100644
index 0000000..62bc7af
--- /dev/null
+++ b/src/api/migrations/0004_snapshotconfig_snapshotrelation.py
@@ -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
index 0000000..559af90
--- /dev/null
+++ b/src/api/migrations/0005_snapshotconfig_delta.py
@@ -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='{}'),
+ ),
+ ]
diff --git a/src/api/models.py b/src/api/models.py
index a1fedfe..e4016aa 100644
--- a/src/api/models.py
+++ b/src/api/models.py
@@ -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,9 +686,41 @@ 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)
job = None
diff --git a/src/notifier/manager.py b/src/notifier/manager.py
index 3361074..f03c2cc 100644
--- a/src/notifier/manager.py
+++ b/src/notifier/manager.py
@@ -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
)
diff --git a/src/templates/notifier/email_fulfilled.txt b/src/templates/notifier/email_fulfilled.txt
index d473961..65593db 100644
--- a/src/templates/notifier/email_fulfilled.txt
+++ b/src/templates/notifier/email_fulfilled.txt
@@ -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 %}
diff --git a/src/templates/snapshot_workflow/steps/meta.html b/src/templates/snapshot_workflow/steps/meta.html
index 2e767cc..cc49691 100644
--- a/src/templates/snapshot_workflow/steps/meta.html
+++ b/src/templates/snapshot_workflow/steps/meta.html
@@ -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 %}
diff --git a/src/templates/snapshot_workflow/steps/select_host.html b/src/templates/snapshot_workflow/steps/select_host.html
index 16dd5d4..27a9238 100644
--- a/src/templates/snapshot_workflow/steps/select_host.html
+++ b/src/templates/snapshot_workflow/steps/select_host.html
@@ -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;
diff --git a/src/workflow/forms.py b/src/workflow/forms.py
index b8c7f66..726e7dd 100644
--- a/src/workflow/forms.py
+++ b/src/workflow/forms.py
@@ -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):
diff --git a/src/workflow/models.py b/src/workflow/models.py
index 495ce07..4e79546 100644
--- a/src/workflow/models.py
+++ b/src/workflow/models.py
@@ -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]
diff --git a/src/workflow/snapshot_workflow.py b/src/workflow/snapshot_workflow.py
index 4ddc397..0d53ed4 100644
--- a/src/workflow/snapshot_workflow.py
+++ b/src/workflow/snapshot_workflow.py
@@ -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):