OverHaul the Snapshot Workflow
[pharos-tools.git] / dashboard / src / api / models.py
1 ##############################################################################
2 # Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others.
3 #
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
9
10
11 from django.contrib.auth.models import User
12 from django.db import models
13 from django.core.exceptions import PermissionDenied
14
15 import json
16 import uuid
17
18 from booking.models import Booking
19 from resource_inventory.models import (
20     Lab,
21     HostProfile,
22     Host,
23     Image,
24     Interface
25 )
26
27
28 class JobStatus(object):
29     NEW = 0
30     CURRENT = 100
31     DONE = 200
32     ERROR = 300
33
34
35 class LabManagerTracker(object):
36
37     @classmethod
38     def get(cls, lab_name, token):
39         """
40         Takes in a lab name (from a url path)
41         returns a lab manager instance for that lab, if it exists
42         """
43         try:
44             lab = Lab.objects.get(name=lab_name)
45         except:
46             raise PermissionDenied("Lab not found")
47         if lab.api_token == token:
48             return LabManager(lab)
49         raise PermissionDenied("Lab not authorized")
50
51
52 class LabManager(object):
53     """
54     This is the class that will ultimately handle all REST calls to
55     lab endpoints.
56     handles jobs, inventory, status, etc
57     may need to create helper classes
58     """
59
60     def __init__(self, lab):
61         self.lab = lab
62
63     def get_profile(self):
64         prof = {}
65         prof['name'] = self.lab.name
66         prof['contact'] = {
67             "phone": self.lab.contact_phone,
68             "email": self.lab.contact_email
69         }
70         prof['host_count'] = []
71         for host in HostProfile.objects.filter(labs=self.lab):
72             count = Host.objects.filter(profile=host, lab=self.lab).count()
73             prof['host_count'].append(
74                 {
75                     "type": host.name,
76                     "count": count
77                 }
78             )
79         return prof
80
81     def get_inventory(self):
82         inventory = {}
83         hosts = Host.objects.filter(lab=self.lab)
84         images = Image.objects.filter(from_lab=self.lab)
85         profiles = HostProfile.objects.filter(labs=self.lab)
86         inventory['hosts'] = self.serialize_hosts(hosts)
87         inventory['images'] = self.serialize_images(images)
88         inventory['host_types'] = self.serialize_host_profiles(profiles)
89         return inventory
90
91     def get_status(self):
92         return {"status": self.lab.status}
93
94     def set_status(self, payload):
95         {}
96
97     def get_current_jobs(self):
98         jobs = Job.objects.filter(booking__lab=self.lab)
99
100         return self.serialize_jobs(jobs, status=JobStatus.CURRENT)
101
102     def get_new_jobs(self):
103         jobs = Job.objects.filter(booking__lab=self.lab)
104
105         return self.serialize_jobs(jobs, status=JobStatus.NEW)
106
107     def get_done_jobs(self):
108         jobs = Job.objects.filter(booking__lab=self.lab)
109
110         return self.serialize_jobs(jobs, status=JobStatus.DONE)
111
112     def get_job(self, jobid):
113         return Job.objects.get(pk=jobid).to_dict()
114
115     def update_job(self, jobid, data):
116         {}
117
118     def serialize_jobs(self, jobs, status=JobStatus.NEW):
119         job_ser = []
120         for job in jobs:
121             jsonized_job = job.get_delta(status)
122             if len(jsonized_job['payload']) < 1:
123                 continue
124             job_ser.append(jsonized_job)
125
126         return job_ser
127
128     def serialize_hosts(self, hosts):
129         host_ser = []
130         for host in hosts:
131             h = {}
132             h['interfaces'] = []
133             h['hostname'] = host.name
134             h['host_type'] = host.profile.name
135             for iface in host.interfaces.all():
136                 eth = {}
137                 eth['mac'] = iface.mac_address
138                 eth['busaddr'] = iface.bus_address
139                 eth['name'] = iface.name
140                 eth['switchport'] = {"switch_name": iface.switch_name, "port_name": iface.port_name}
141                 h['interfaces'].append(eth)
142         return host_ser
143
144     def serialize_images(self, images):
145         images_ser = []
146         for image in images:
147             images_ser.append(
148                 {
149                     "name": image.name,
150                     "lab_id": image.lab_id,
151                     "dashboard_id": image.id
152                 }
153             )
154         return images_ser
155
156     def serialize_host_profiles(self, profiles):
157         profile_ser = []
158         for profile in profiles:
159             p = {}
160             p['cpu'] = {
161                 "cores": profile.cpuprofile.first().cores,
162                 "arch": profile.cpuprofile.first().architecture,
163                 "cpus": profile.cpuprofile.first().cpus,
164             }
165             p['disks'] = []
166             for disk in profile.storageprofile.all():
167                 d = {
168                     "size": disk.size,
169                     "type": disk.media_type,
170                     "name": disk.name
171                 }
172                 p['disks'].append(d)
173             p['description'] = profile.description
174             p['interfaces'] = []
175             for iface in profile.interfaceprofile.all():
176                 p['interfaces'].append(
177                     {
178                         "speed": iface.speed,
179                         "name": iface.name
180                     }
181                 )
182
183             p['ram'] = {"amount": profile.ramprofile.first().amount}
184             p['name'] = profile.name
185             profile_ser.append(p)
186         return profile_ser
187
188
189 class Job(models.Model):
190     """
191     This is the class that is serialized and put into the api
192     """
193     booking = models.OneToOneField(Booking, on_delete=models.CASCADE, null=True)
194     status = models.IntegerField(default=JobStatus.NEW)
195     complete = models.BooleanField(default=False)
196
197     def to_dict(self):
198         d = {}
199         j = {}
200         j['id'] = self.id
201         for relation in AccessRelation.objects.filter(job=self):
202             if 'access' not in d:
203                 d['access'] = {}
204             d['access'][relation.task_id] = relation.config.to_dict()
205         for relation in SoftwareRelation.objects.filter(job=self):
206             if 'software' not in d:
207                 d['software'] = {}
208             d['software'][relation.task_id] = relation.config.to_dict()
209         for relation in HostHardwareRelation.objects.filter(job=self):
210             if 'hardware' not in d:
211                 d['hardware'] = {}
212             d['hardware'][relation.task_id] = relation.config.to_dict()
213         for relation in HostNetworkRelation.objects.filter(job=self):
214             if 'network' not in d:
215                 d['network'] = {}
216             d['network'][relation.task_id] = relation.config.to_dict()
217         for relation in SnapshotRelation.objects.filter(job=self):
218             if 'snapshot' not in d:
219                 d['snapshot'] = {}
220             d['snapshot'][relation.task_id] = relation.config.to_dict()
221
222         j['payload'] = d
223
224         return j
225
226     def get_tasklist(self, status="all"):
227         tasklist = []
228         clist = [
229             HostHardwareRelation,
230             AccessRelation,
231             HostNetworkRelation,
232             SoftwareRelation,
233             SnapshotRelation
234         ]
235         if status == "all":
236             for cls in clist:
237                 tasklist += list(cls.objects.filter(job=self))
238         else:
239             for cls in clist:
240                 tasklist += list(cls.objects.filter(job=self).filter(status=status))
241         return tasklist
242
243     def is_fulfilled(self):
244         """
245         This method should return true if all of the job's tasks are done,
246         and false otherwise
247         """
248         my_tasks = self.get_tasklist()
249         for task in my_tasks:
250             if task.status != JobStatus.DONE:
251                 return False
252         return True
253
254     def get_delta(self, status):
255         d = {}
256         j = {}
257         j['id'] = self.id
258         for relation in AccessRelation.objects.filter(job=self).filter(status=status):
259             if 'access' not in d:
260                 d['access'] = {}
261             d['access'][relation.task_id] = relation.config.get_delta()
262         for relation in SoftwareRelation.objects.filter(job=self).filter(status=status):
263             if 'software' not in d:
264                 d['software'] = {}
265             d['software'][relation.task_id] = relation.config.get_delta()
266         for relation in HostHardwareRelation.objects.filter(job=self).filter(status=status):
267             if 'hardware' not in d:
268                 d['hardware'] = {}
269             d['hardware'][relation.task_id] = relation.config.get_delta()
270         for relation in HostNetworkRelation.objects.filter(job=self).filter(status=status):
271             if 'network' not in d:
272                 d['network'] = {}
273             d['network'][relation.task_id] = relation.config.get_delta()
274         for relation in SnapshotRelation.objects.filter(job=self).filter(status=status):
275             if 'snapshot' not in d:
276                 d['snapshot'] = {}
277             d['snapshot'][relation.task_id] = relation.config.get_delta()
278
279         j['payload'] = d
280         return j
281
282     def to_json(self):
283         return json.dumps(self.to_dict())
284
285
286 class TaskConfig(models.Model):
287     def to_dict(self):
288         pass
289
290     def get_delta(self):
291         pass
292
293     def to_json(self):
294         return json.dumps(self.to_dict())
295
296     def clear_delta(self):
297         self.delta = '{}'
298
299
300 class OpnfvApiConfig(models.Model):
301
302     installer = models.CharField(max_length=100)
303     scenario = models.CharField(max_length=100)
304     roles = models.ManyToManyField(Host)
305     delta = models.TextField()
306
307     def to_dict(self):
308         d = {}
309         if self.installer:
310             d['installer'] = self.installer
311         if self.scenario:
312             d['scenario'] = self.scenario
313
314         hosts = self.roles.all()
315         if hosts.exists():
316             d['roles'] = []
317         for host in self.roles.all():
318             d['roles'].append({host.labid: host.config.opnfvRole.name})
319
320         return d
321
322     def to_json(self):
323         return json.dumps(self.to_dict())
324
325     def set_installer(self, installer):
326         self.installer = installer
327         d = json.loads(self.delta)
328         d['installer'] = installer
329         self.delta = json.dumps(d)
330
331     def set_scenario(self, scenario):
332         self.scenario = scenario
333         d = json.loads(self.delta)
334         d['scenario'] = scenario
335         self.delta = json.dumps(d)
336
337     def add_role(self, host):
338         self.roles.add(host)
339         d = json.loads(self.delta)
340         if 'role' not in d:
341             d['role'] = []
342         d['roles'].append({host.labid: host.config.opnfvRole.name})
343         self.delta = json.dumps(d)
344
345     def clear_delta(self):
346         self.delta = '{}'
347
348     def get_delta(self):
349         if not self.delta:
350             self.delta = self.to_json()
351             self.save()
352         return json.loads(self.delta)
353
354
355 class AccessConfig(TaskConfig):
356     access_type = models.CharField(max_length=50)
357     user = models.ForeignKey(User, on_delete=models.CASCADE)
358     revoke = models.BooleanField(default=False)
359     context = models.TextField(default="")
360     delta = models.TextField(default="{}")
361
362     def to_dict(self):
363         d = {}
364         d['access_type'] = self.access_type
365         d['user'] = self.user.id
366         d['revoke'] = self.revoke
367         try:
368             d['context'] = json.loads(self.context)
369         except:
370             pass
371         return d
372
373     def get_delta(self):
374         if not self.delta:
375             self.delta = self.to_json()
376             self.save()
377         d = json.loads(self.delta)
378         d["lab_token"] = self.accessrelation.lab_token
379
380         return d
381
382     def to_json(self):
383         return json.dumps(self.to_dict())
384
385     def clear_delta(self):
386         d = {}
387         d["lab_token"] = self.accessrelation.lab_token
388         self.delta = json.dumps(d)
389
390     def set_access_type(self, access_type):
391         self.access_type = access_type
392         d = json.loads(self.delta)
393         d['access_type'] = access_type
394         self.delta = json.dumps(d)
395
396     def set_user(self, user):
397         self.user = user
398         d = json.loads(self.delta)
399         d['user'] = self.user.id
400         self.delta = json.dumps(d)
401
402     def set_revoke(self, revoke):
403         self.revoke = revoke
404         d = json.loads(self.delta)
405         d['revoke'] = revoke
406         self.delta = json.dumps(d)
407
408     def set_context(self, context):
409         self.context = json.dumps(context)
410         d = json.loads(self.delta)
411         d['context'] = context
412         self.delta = json.dumps(d)
413
414
415 class SoftwareConfig(TaskConfig):
416     """
417     handled opnfv installations, etc
418     """
419     opnfv = models.ForeignKey(OpnfvApiConfig, on_delete=models.CASCADE)
420
421     def to_dict(self):
422         d = {}
423         if self.opnfv:
424             d['opnfv'] = self.opnfv.to_dict()
425
426         d["lab_token"] = self.softwarerelation.lab_token
427         self.delta = json.dumps(d)
428
429         return d
430
431     def get_delta(self):
432         d = {}
433         d['opnfv'] = self.opnfv.get_delta()
434         d['lab_token'] = self.softwarerelation.lab_token
435
436         return d
437
438     def clear_delta(self):
439         self.opnfv.clear_delta()
440
441     def to_json(self):
442         return json.dumps(self.to_dict())
443
444
445 class HardwareConfig(TaskConfig):
446     """
447     handles imaging, user accounts, etc
448     """
449     image = models.CharField(max_length=100, default="defimage")
450     power = models.CharField(max_length=100, default="off")
451     hostname = models.CharField(max_length=100, default="hostname")
452     ipmi_create = models.BooleanField(default=False)
453     delta = models.TextField()
454
455     def to_dict(self):
456         d = {}
457         d['image'] = self.image
458         d['power'] = self.power
459         d['hostname'] = self.hostname
460         d['ipmi_create'] = str(self.ipmi_create)
461         d['id'] = self.hosthardwarerelation.host.labid
462         return d
463
464     def to_json(self):
465         return json.dumps(self.to_dict())
466
467     def get_delta(self):
468         if not self.delta:
469             self.delta = self.to_json()
470             self.save()
471         d = json.loads(self.delta)
472         d['lab_token'] = self.hosthardwarerelation.lab_token
473         return d
474
475     def clear_delta(self):
476         d = {}
477         d["id"] = self.hosthardwarerelation.host.labid
478         d["lab_token"] = self.hosthardwarerelation.lab_token
479         self.delta = json.dumps(d)
480
481     def set_image(self, image):
482         self.image = image
483         d = json.loads(self.delta)
484         d['image'] = self.image
485         self.delta = json.dumps(d)
486
487     def set_power(self, power):
488         self.power = power
489         d = json.loads(self.delta)
490         d['power'] = power
491         self.delta = json.dumps(d)
492
493     def set_hostname(self, hostname):
494         self.hostname = hostname
495         d = json.loads(self.delta)
496         d['hostname'] = hostname
497         self.delta = json.dumps(d)
498
499     def set_ipmi_create(self, ipmi_create):
500         self.ipmi_create = ipmi_create
501         d = json.loads(self.delta)
502         d['ipmi_create'] = ipmi_create
503         self.delta = json.dumps(d)
504
505
506 class NetworkConfig(TaskConfig):
507     """
508     handles network configuration
509     """
510     interfaces = models.ManyToManyField(Interface)
511     delta = models.TextField()
512
513     def to_dict(self):
514         d = {}
515         hid = self.hostnetworkrelation.host.labid
516         d[hid] = {}
517         for interface in self.interfaces.all():
518             d[hid][interface.mac_address] = []
519             for vlan in interface.config.all():
520                 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
521
522         return d
523
524     def to_json(self):
525         return json.dumps(self.to_dict())
526
527     def get_delta(self):
528         if not self.delta:
529             self.delta = self.to_json()
530             self.save()
531         d = json.loads(self.delta)
532         d['lab_token'] = self.hostnetworkrelation.lab_token
533         return d
534
535     def clear_delta(self):
536         self.delta = json.dumps(self.to_dict())
537         self.save()
538
539     def add_interface(self, interface):
540         self.interfaces.add(interface)
541         d = json.loads(self.delta)
542         hid = self.hostnetworkrelation.host.labid
543         if hid not in d:
544             d[hid] = {}
545         d[hid][interface.mac_address] = []
546         for vlan in interface.config.all():
547             d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
548         self.delta = json.dumps(d)
549
550
551 class SnapshotConfig(TaskConfig):
552
553     host = models.ForeignKey(Host, null=True, on_delete=models.DO_NOTHING)
554     image = models.IntegerField(null=True)
555     dashboard_id = models.IntegerField()
556     delta = models.TextField(default="{}")
557
558     def to_dict(self):
559         d = {}
560         if self.host:
561             d['host'] = self.host.labid
562         if self.image:
563             d['image'] = self.image
564         d['dashboard_id'] = self.dashboard_id
565         return d
566
567     def to_json(self):
568         return json.dumps(self.to_dict())
569
570     def get_delta(self):
571         if not self.delta:
572             self.delta = self.to_json()
573             self.save()
574         d = json.loads(self.delta)
575         return d
576
577     def clear_delta(self):
578         self.delta = json.dumps(self.to_dict())
579         self.save()
580
581     def set_host(self, host):
582         self.host = host
583         d = json.loads(self.delta)
584         d['host'] = host.labid
585         self.delta = json.dumps(d)
586
587     def set_image(self, image):
588         self.image = image
589         d = json.loads(self.delta)
590         d['image'] = self.image
591         self.delta = json.dumps(d)
592
593     def clear_image(self):
594         self.image = None
595         d = json.loads(self.delta)
596         d.pop("image", None)
597         self.delta = json.dumps(d)
598
599     def set_dashboard_id(self, dash):
600         self.dashboard_id = dash
601         d = json.loads(self.delta)
602         d['dashboard_id'] = self.dashboard_id
603         self.delta = json.dumps(d)
604
605
606 def get_task(task_id):
607     for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation]:
608         try:
609             ret = taskclass.objects.get(task_id=task_id)
610             return ret
611         except taskclass.DoesNotExist:
612             pass
613     from django.core.exceptions import ObjectDoesNotExist
614     raise ObjectDoesNotExist("Could not find matching TaskRelation instance")
615
616
617 def get_task_uuid():
618     return str(uuid.uuid4())
619
620
621 class TaskRelation(models.Model):
622     status = models.IntegerField(default=JobStatus.NEW)
623     job = models.ForeignKey(Job, on_delete=models.CASCADE)
624     config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE)
625     task_id = models.CharField(default=get_task_uuid, max_length=37)
626     lab_token = models.CharField(default="null", max_length=50)
627     message = models.TextField(default="")
628
629     def delete(self, *args, **kwargs):
630         self.config.delete()
631         return super(self.__class__, self).delete(*args, **kwargs)
632
633     def type_str(self):
634         return "Generic Task"
635
636     class Meta:
637         abstract = True
638
639
640 class AccessRelation(TaskRelation):
641     config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE)
642
643     def type_str(self):
644         return "Access Task"
645
646     def delete(self, *args, **kwargs):
647         self.config.delete()
648         return super(self.__class__, self).delete(*args, **kwargs)
649
650
651 class SoftwareRelation(TaskRelation):
652     config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE)
653
654     def type_str(self):
655         return "Software Configuration Task"
656
657     def delete(self, *args, **kwargs):
658         self.config.delete()
659         return super(self.__class__, self).delete(*args, **kwargs)
660
661
662 class HostHardwareRelation(TaskRelation):
663     host = models.ForeignKey(Host, on_delete=models.CASCADE)
664     config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE)
665
666     def type_str(self):
667         return "Hardware Configuration Task"
668
669     def get_delta(self):
670         return self.config.to_dict()
671
672     def delete(self, *args, **kwargs):
673         self.config.delete()
674         return super(self.__class__, self).delete(*args, **kwargs)
675
676
677 class HostNetworkRelation(TaskRelation):
678     host = models.ForeignKey(Host, on_delete=models.CASCADE)
679     config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
680
681     def type_str(self):
682         return "Network Configuration Task"
683
684     def delete(self, *args, **kwargs):
685         self.config.delete()
686         return super(self.__class__, self).delete(*args, **kwargs)
687
688
689 class SnapshotRelation(TaskRelation):
690     snapshot = models.ForeignKey(Image, on_delete=models.CASCADE)
691     config = models.OneToOneField(SnapshotConfig, on_delete=models.CASCADE)
692
693     def type_str(self):
694         return "Snapshot Task"
695
696     def get_delta(self):
697         return self.config.to_dict()
698
699     def delete(self, *args, **kwargs):
700         self.config.delete()
701         return super(self.__class__, self).delete(*args, **kwargs)
702
703
704 class JobFactory(object):
705
706     @classmethod
707     def makeSnapshotTask(cls, image, booking, host):
708         relation = SnapshotRelation()
709         job = Job.objects.get(booking=booking)
710         config = SnapshotConfig.objects.create(dashboard_id=image.id)
711
712         relation.job = job
713         relation.config = config
714         relation.config.save()
715         relation.config = relation.config
716         relation.snapshot = image
717         relation.save()
718
719         config.clear_delta()
720         config.set_host(host)
721         config.save()
722
723     @classmethod
724     def makeCompleteJob(cls, booking):
725         hosts = Host.objects.filter(bundle=booking.resource)
726         job = None
727         try:
728             job = Job.objects.get(booking=booking)
729         except:
730             job = Job.objects.create(status=JobStatus.NEW, booking=booking)
731         cls.makeHardwareConfigs(
732             hosts=hosts,
733             job=job
734         )
735         cls.makeNetworkConfigs(
736             hosts=hosts,
737             job=job
738         )
739         cls.makeSoftware(
740             hosts=hosts,
741             job=job
742         )
743         all_users = list(booking.collaborators.all())
744         all_users.append(booking.owner)
745         cls.makeAccessConfig(
746             users=all_users,
747             access_type="vpn",
748             revoke=False,
749             job=job
750         )
751         for user in all_users:
752             try:
753                 cls.makeAccessConfig(
754                     users=[user],
755                     access_type="ssh",
756                     revoke=False,
757                     job=job,
758                     context={
759                         "key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"),
760                         "hosts": [host.labid for host in hosts]
761                     }
762                 )
763             except Exception:
764                 continue
765
766     @classmethod
767     def makeHardwareConfigs(cls, hosts=[], job=Job()):
768         for host in hosts:
769             hardware_config = None
770             try:
771                 hardware_config = HardwareConfig.objects.get(relation__host=host)
772             except:
773                 hardware_config = HardwareConfig()
774
775             relation = HostHardwareRelation()
776             relation.host = host
777             relation.job = job
778             relation.config = hardware_config
779             relation.config.save()
780             relation.config = relation.config
781             relation.save()
782
783             hardware_config.clear_delta()
784             hardware_config.set_image(host.config.image.lab_id)
785             hardware_config.set_hostname(host.template.resource.name)
786             hardware_config.set_power("on")
787             hardware_config.set_ipmi_create(True)
788             hardware_config.save()
789
790     @classmethod
791     def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False):
792         for user in users:
793             relation = AccessRelation()
794             relation.job = job
795             config = AccessConfig()
796             config.access_type = access_type
797             config.user = user
798             config.save()
799             relation.config = config
800             relation.save()
801             config.clear_delta()
802             if context:
803                 config.set_context(context)
804             config.set_access_type(access_type)
805             config.set_revoke(revoke)
806             config.set_user(user)
807             config.save()
808
809     @classmethod
810     def makeNetworkConfigs(cls, hosts=[], job=Job()):
811         for host in hosts:
812             network_config = None
813             try:
814                 network_config = NetworkConfig.objects.get(relation__host=host)
815             except:
816                 network_config = NetworkConfig.objects.create()
817
818             relation = HostNetworkRelation()
819             relation.host = host
820             relation.job = job
821             network_config.save()
822             relation.config = network_config
823             relation.save()
824             network_config.clear_delta()
825
826             for interface in host.interfaces.all():
827                 network_config.add_interface(interface)
828             network_config.save()
829
830     @classmethod
831     def makeSoftware(cls, hosts=[], job=Job()):
832         def init_config(host):
833             opnfv_config = OpnfvApiConfig()
834             if host is not None:
835                 opnfv = host.config.bundle.opnfv_config.first()
836                 opnfv_config.installer = opnfv.installer.name
837                 opnfv_config.scenario = opnfv.scenario.name
838             opnfv_config.save()
839             return opnfv_config
840
841         try:
842             host = None
843             if len(hosts) > 0:
844                 host = hosts[0]
845             opnfv_config = init_config(host)
846
847             for host in hosts:
848                 opnfv_config.roles.add(host)
849             software_config = SoftwareConfig.objects.create(opnfv=opnfv_config)
850             software_config.save()
851             software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
852             software_relation.save()
853             return software_relation
854         except:
855             return None