diff options
-rw-r--r-- | src/api/migrations/0017_cloudinitfile.py | 25 | ||||
-rw-r--r-- | src/api/models.py | 109 | ||||
-rw-r--r-- | src/api/urls.py | 4 | ||||
-rw-r--r-- | src/api/views.py | 17 | ||||
-rw-r--r-- | src/dashboard/utils.py | 10 |
5 files changed, 161 insertions, 4 deletions
diff --git a/src/api/migrations/0017_cloudinitfile.py b/src/api/migrations/0017_cloudinitfile.py new file mode 100644 index 0000000..f14aea1 --- /dev/null +++ b/src/api/migrations/0017_cloudinitfile.py @@ -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')), + ], + ), + ] diff --git a/src/api/models.py b/src/api/models.py index d1bb692..36d1b8c 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -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( @@ -1037,6 +1138,12 @@ class JobFactory(object): 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()): """ Create and save HardwareConfig. diff --git a/src/api/urls.py b/src/api/urls.py index bae86ea..7adeef6 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -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), diff --git a/src/api/views.py b/src/api/views.py index 2e5f33f..3a3effa 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -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') diff --git a/src/dashboard/utils.py b/src/dashboard/utils.py index d6b697a..e9ecb4e 100644 --- a/src/dashboard/utils.py +++ b/src/dashboard/utils.py @@ -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() |