Draft for cloud-init file generation 62/72662/6
authorSawyer Bergeron <sbergeron@iol.unh.edu>
Tue, 8 Jun 2021 15:15:56 +0000 (11:15 -0400)
committerSawyer Bergeron <sbergeron@iol.unh.edu>
Mon, 14 Jun 2021 15:22:47 +0000 (11:22 -0400)
Signed-off-by: Sawyer Bergeron <sbergeron@iol.unh.edu>
Signed-off-by: Sawyer Bergeron <sbergeron@iol.unh.edu>
Change-Id: I07f3a4a1ab67531cba2cc7e3de22e9bb860706e1
Signed-off-by: Sawyer Bergeron <sbergeron@iol.unh.edu>
src/api/migrations/0017_cloudinitfile.py [new file with mode: 0644]
src/api/models.py
src/api/urls.py
src/api/views.py
src/dashboard/utils.py

diff --git a/src/api/migrations/0017_cloudinitfile.py b/src/api/migrations/0017_cloudinitfile.py
new file mode 100644 (file)
index 0000000..f14aea1
--- /dev/null
@@ -0,0 +1,25 @@
+# Generated by Django 2.2 on 2021-06-11 20:42
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('resource_inventory', '0017_auto_20201218_1516'),
+        ('booking', '0008_auto_20201109_1947'),
+        ('api', '0016_auto_20201109_2149'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='CloudInitFile',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('resource_id', models.CharField(max_length=200)),
+                ('booking', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='booking.Booking')),
+                ('rconfig', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.ResourceConfiguration')),
+            ],
+        ),
+    ]
index d1bb692..36d1b8c 100644 (file)
@@ -18,6 +18,7 @@ from django.utils import timezone
 
 import json
 import uuid
+import yaml
 
 from booking.models import Booking
 from resource_inventory.models import (
@@ -29,7 +30,8 @@ from resource_inventory.models import (
     RemoteInfo,
     OPNFVConfig,
     ConfigState,
-    ResourceQuery
+    ResourceQuery,
+    ResourceConfiguration
 )
 from resource_inventory.idf_templater import IDFTemplater
 from resource_inventory.pdf_templater import PDFTemplater
@@ -336,6 +338,99 @@ class LabManager(object):
             profile_ser.append(p)
         return profile_ser
 
+class CloudInitFile(models.Model):
+    resource_id = models.CharField(max_length=200)
+    booking = models.ForeignKey(Booking, on_delete=models.CASCADE)
+    rconfig = models.ForeignKey(ResourceConfiguration, on_delete=models.CASCADE)
+
+    def _normalize_username(self, username: str) -> str:
+        # TODO: make usernames posix compliant
+        return username
+
+    def _get_ssh_string(self, username: str) -> str:
+        user = User.objects.get(username=username)
+        uprofile = user.userprofile
+
+        ssh_file = uprofile.ssh_public_key
+
+        escaped_file = ssh_file.open().read().decode(encoding="UTF-8").replace("\n", " ")
+
+        return escaped_file
+
+    def _serialize_users(self):
+        """
+        returns the dictionary to be placed behind the `users` field of the toplevel c-i dict
+        """
+        user_array = ["default"]
+        users = list(self.booking.collaborators.all())
+        users.append(self.booking.owner.userprofile)
+        for collaborator in users:
+            userdict = {}
+
+            # TODO: validate if usernames are valid as linux usernames (and provide an override potentially)
+            userdict['name'] = self._normalize_username(collaborator.user.username)
+
+            userdict['groups'] = "sudo"
+            userdict['sudo'] = "ALL=(ALL) NOPASSWD:ALL"
+
+            userdict['ssh_authorized_keys'] = [self._get_ssh_string(collaborator.user.username)]
+
+            user_array.append(userdict)
+
+        return user_array
+
+    def _serialize_netconf_v1(self):
+        config_arr = []
+
+        for interface in self._resource().interfaces.all():
+            interface_name = interface.profile.name
+            interface_mac = interface.mac_address
+
+            for vlan in interface.config.all():
+                vlan_dict_entry = {'type': 'vlan'}
+                vlan_dict_entry['name'] = str(interface_name) + "." + str(vlan.vlan_id)
+                vlan_dict_entry['link'] = str(interface_name)
+                vlan_dict_entry['vlan_id'] = int(vlan.vlan_id)
+                vlan_dict_entry['mac_address'] = str(interface_mac)
+                #vlan_dict_entry['mtu'] = # TODO, determine override MTU if needed
+
+                config_arr.append(vlan_dict_entry)
+
+        ns_dict = {
+                'type': 'nameserver',
+                'address': ['10.64.0.1', '8.8.8.8']
+        }
+
+        config_arr.append(ns_dict)
+
+        full_dict = {'version': 1, 'config': config_arr}
+
+        return full_dict
+
+    @classmethod
+    def get(booking_id: int, resource_lab_id: str):
+        return CloudInitFile.objects.get(resource_id=resource_lab_id, booking__id=booking_id)
+
+    def _resource(self):
+        return ResourceQuery.get(labid=self.resource_id, lab=self.booking.lab)
+
+    def _get_facts(self):
+        resource = self._resource()
+
+        hostname = self.rconfig.name
+        iface_configs = for_config.interface_configs.all()
+
+    def _to_dict(self):
+        main_dict = {}
+
+        main_dict['users'] = self._serialize_users()
+        main_dict['network'] = self._serialize_netconf_v1()
+        main_dict['hostname'] = self.rconfig.name
+
+        return main_dict
+
+    def serialize(self) -> str:
+        return yaml.dump(self._to_dict())
 
 class Job(models.Model):
     """
@@ -670,8 +765,10 @@ class HardwareConfig(TaskConfig):
         return self.get_delta()
 
     def get_delta(self):
+        # TODO: grab the CloudInitFile urls from self.hosthardwarerelation.get_resource()
         return self.format_delta(
             self.hosthardwarerelation.get_resource().get_configuration(self.state),
+            self.cloudinit_file.get_delta_url(),
             self.hosthardwarerelation.lab_token)
 
 
@@ -1013,6 +1110,10 @@ class JobFactory(object):
             booking=booking,
             job=job
         )
+        cls.makeCloudInitFiles(
+            resources=resources,
+            job=job
+        )
         all_users = list(booking.collaborators.all())
         all_users.append(booking.owner)
         cls.makeAccessConfig(
@@ -1036,6 +1137,12 @@ class JobFactory(object):
             except Exception:
                 continue
 
+    @classmethod
+    def makeCloudInitFiles(cls, resources=[], job=Job()):
+        for res in resources:
+            cif = CloudInitFile.objects.create(resource_id=res.labid, booking=job.booking, rconfig=res.config)
+            cif.save()
+
     @classmethod
     def makeHardwareConfigs(cls, resources=[], job=Job()):
         """
index bae86ea..7adeef6 100644 (file)
@@ -45,7 +45,8 @@ from api.views import (
     lab_users,
     lab_user,
     GenerateTokenView,
-    analytics_job
+    analytics_job,
+    resource_cidata,
 )
 
 urlpatterns = [
@@ -59,6 +60,7 @@ urlpatterns = [
     path('labs/<slug:lab_name>/booking/<int:booking_id>/idf', get_idf, name="get-idf"),
     path('labs/<slug:lab_name>/jobs/<int:job_id>', specific_job),
     path('labs/<slug:lab_name>/jobs/<int:job_id>/<slug:task_id>', specific_task),
+    path('labs/<slug:lab_name>/jobs/<int:job_id>/cidata/<slug:resource_id', resource_cidata),
     path('labs/<slug:lab_name>/jobs/new', new_jobs),
     path('labs/<slug:lab_name>/jobs/current', current_jobs),
     path('labs/<slug:lab_name>/jobs/done', done_jobs),
index 2e5f33f..3a3effa 100644 (file)
@@ -25,7 +25,7 @@ from api.serializers.old_serializers import UserSerializer
 from api.forms import DowntimeForm
 from account.models import UserProfile
 from booking.models import Booking
-from api.models import LabManagerTracker, get_task
+from api.models import LabManagerTracker, get_task, CloudInitFile
 from notifier.manager import NotificationHandler
 from analytics.models import ActiveVPNUser
 import json
@@ -166,6 +166,21 @@ def specific_job(request, lab_name="", job_id=""):
         return JsonResponse(lab_manager.update_job(job_id, request.POST), safe=False)
     return JsonResponse(lab_manager.get_job(job_id), safe=False)
 
+@csrf_exempt
+def resource_cidata(request, lab_name="", job_id="", resource_id=""):
+    lab_token = request.META.get('HTTP_AUTH_TOKEN')
+    lab_manager = LabManagerTracker.get(lab_name, lab_token)
+
+    job = lab_manager.get_job(job_id)
+
+    cifile = None
+    try:
+        cifile = CloudInitFile.get(job.booking.id, resource_id)
+    except ObjectDoesNotExist:
+        return HttpResponseNotFound("Could not find a matching resource by id " + str(resource_id))
+
+    return HttpResponse(cifile.serialize(), status=200)
+
 
 def new_jobs(request, lab_name=""):
     lab_token = request.META.get('HTTP_AUTH_TOKEN')
index d6b697a..e9ecb4e 100644 (file)
@@ -7,7 +7,7 @@
 # http://www.apache.org/licenses/LICENSE-2.0
 ##############################################################################
 
-from django.core.exceptions import ObjectDoesNotExist
+from django.core.exceptions import (ObjectDoesNotExist, MultipleObjectsReturned)
 
 
 class AbstractModelQuery():
@@ -38,7 +38,15 @@ class AbstractModelQuery():
 
     @classmethod
     def get(cls, *args, **kwargs):
+        """
+        Gets a single matching resource
+        Throws ObjectDoesNotExist if none found matching, or MultipleObjectsReturned if
+        the query does not narrow to a single object
+        """
         try:
+            ls = cls.filter(*args, **kwargs)
+            if len(ls) >  1:
+                raise MultipleObjectsReturned()
             return cls.filter(*args, **kwargs)[0]
         except IndexError:
             raise ObjectDoesNotExist()