rebuilt host import admin util
[laas.git] / src / dashboard / admin_utils.py
1 ##############################################################################
2 # Copyright (c) 2021 Sawyer Bergeron 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 from resource_inventory.models import (
11     ResourceTemplate,
12     Image,
13     Server,
14     ResourceBundle,
15     ResourceProfile,
16     InterfaceProfile,
17     PhysicalNetwork,
18     ResourceConfiguration,
19     NetworkConnection,
20     InterfaceConfiguration,
21     Network,
22     DiskProfile,
23     CpuProfile,
24     RamProfile,
25     Interface,
26     CloudInitFile,
27 )
28
29 import json
30 import yaml
31 import sys
32 import inspect
33 import pydoc
34 import csv
35
36 from django.contrib.auth.models import User
37
38 from account.models import (
39     Lab,
40     PublicNetwork
41 )
42
43 from resource_inventory.resource_manager import ResourceManager
44 from resource_inventory.pdf_templater import PDFTemplater
45
46 from booking.quick_deployer import update_template
47
48 from datetime import timedelta, date, datetime, timezone
49
50 from booking.models import Booking
51 from notifier.manager import NotificationHandler
52 from api.models import JobFactory
53
54 from api.models import JobStatus, Job, GeneratedCloudConfig
55
56
57 def print_div():
58     """
59     Utility function for printing dividers, does nothing directly useful as a utility
60     """
61     print("=" * 68)
62
63
64 def book_host(owner_username, host_labid, lab_username, hostname, image_id, template_name, length_days=21, collaborator_usernames=[], purpose="internal", project="LaaS"):
65     """
66     creates a quick booking using the given host
67
68     @owner_username is the simple username for the user who will own the resulting booking.
69     Do not set this to a lab username!
70
71     @image_id is the django id of the image in question, NOT the labid of the image.
72     Query Image objects by their public status and compatible host types
73
74     @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
75
76     @lab_username for iol is `unh_iol`, other labs will be documented here
77
78     @hostname the hostname that the resulting host should have set
79
80     @template_name the name of the (public, or user accessible) template to use for this booking
81
82     @length_days how long the booking should be, no hard limit currently
83
84     @collaborator_usernames a list of usernames for collaborators to the booking
85
86     @purpose what this booking will be used for
87
88     @project what project/group this booking is on behalf of or the owner represents
89     """
90     lab = Lab.objects.get(lab_user__username=lab_username)
91     host = Server.objects.filter(lab=lab).get(labid=host_labid)
92     if host.booked:
93         print("Can't book host, already marked as booked")
94         return
95     else:
96         host.booked = True
97         host.save()
98
99     template = ResourceTemplate.objects.filter(public=True).get(name=template_name)
100     image = Image.objects.get(id=image_id)
101
102     owner = User.objects.get(username=owner_username)
103
104     new_template = update_template(template, image, hostname, owner)
105
106     rmanager = ResourceManager.getInstance()
107
108     vlan_map = rmanager.get_vlans(new_template)
109
110     # only a single host so can reuse var for iter here
111     resource_bundle = ResourceBundle.objects.create(template=new_template)
112     res_configs = new_template.getConfigs()
113
114     for config in res_configs:
115         try:
116             host.bundle = resource_bundle
117             host.config = config
118             rmanager.configureNetworking(resource_bundle, host, vlan_map)
119             host.save()
120         except Exception:
121             host.booked = False
122             host.save()
123             print("Failed to book host due to error configuring it")
124             return
125
126     new_template.save()
127
128     booking = Booking.objects.create(
129         purpose=purpose,
130         project=project,
131         lab=lab,
132         owner=owner,
133         start=timezone.now(),
134         end=timezone.now() + timedelta(days=int(length_days)),
135         resource=resource_bundle,
136         opnfv_config=None
137     )
138
139     booking.pdf = PDFTemplater.makePDF(booking)
140
141     booking.save()
142
143     for collaborator_username in collaborator_usernames:
144         try:
145             user = User.objects.get(username=collaborator_username)
146             booking.collaborators.add(user)
147         except Exception:
148             print("couldn't add user with username ", collaborator_username)
149
150     booking.save()
151
152     JobFactory.makeCompleteJob(booking)
153     NotificationHandler.notify_new_booking(booking)
154
155
156 def mark_working(host_labid, lab_username, working=True):
157     """
158     Mark a host working/not working so that it is either bookable or hidden in the dashboard.
159
160     @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
161
162     @lab_username: param of the form `unh_iol` or similar
163
164     @working: bool, whether by the end of execution the host should be considered working or not working
165     """
166
167     lab = Lab.objects.get(lab_user__username=lab_username)
168     server = Server.objects.filter(lab=lab).get(labid=host_labid)
169     print("changing server working status from ", server.working, "to", working)
170     server.working = working
171     server.save()
172
173
174 def mark_booked(host_labid, lab_username, booked=True):
175     """
176     Mark a host as booked/unbooked
177
178     @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
179
180     @lab_username: param of the form `unh_iol` or similar
181
182     @working: bool, whether by the end of execution the host should be considered booked or not booked
183     """
184
185     lab = Lab.objects.get(lab_user__username=lab_username)
186     server = Server.objects.filter(lab=lab).get(labid=host_labid)
187     print("changing server booked status from ", server.booked, "to", booked)
188     server.booked = booked
189     server.save()
190
191
192 def get_host(host_labid, lab_username):
193     """
194     Returns host filtered by lab and then unique id within lab
195
196     @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
197
198     @lab_username: param of the form `unh_iol` or similar
199     """
200     lab = Lab.objects.get(lab_user__username=lab_username)
201     return Server.objects.filter(lab=lab).get(labid=host_labid)
202
203
204 def get_info(host_labid, lab_username):
205     """
206     Returns various information on the host queried by the given parameters
207
208     @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
209
210     @lab_username: param of the form `unh_iol` or similar
211     """
212     info = {}
213     host = get_host(host_labid, lab_username)
214     info['host_labid'] = host_labid
215     info['booked'] = host.booked
216     info['working'] = host.working
217     info['profile'] = str(host.profile)
218     if host.bundle:
219         binfo = {}
220         info['bundle'] = binfo
221     if host.config:
222         cinfo = {}
223         info['config'] = cinfo
224
225     return info
226
227
228 class CumulativeData:
229     use_days = 0
230     count_bookings = 0
231     count_extensions = 0
232
233     def __init__(self, file_writer):
234         self.file_writer = file_writer
235
236     def account(self, booking, usage_days):
237         self.count_bookings += 1
238         self.count_extensions += booking.ext_count
239         self.use_days += usage_days
240
241     def write_cumulative(self):
242         self.file_writer.writerow([])
243         self.file_writer.writerow([])
244         self.file_writer.writerow(['Lab Use Days', 'Count of Bookings', 'Total Extensions Used'])
245         self.file_writer.writerow([self.use_days, self.count_bookings, (self.count_bookings * 2) - self.count_extensions])
246
247
248 def get_years_booking_data(start_year=None, end_year=None):
249     """
250     Outputs yearly booking information from the past 'start_year' years (default: current year)
251     until the last day of the end year (default current year) as a csv file.
252     """
253     if start_year is None and end_year is None:
254         start = datetime.combine(date(datetime.now().year, 1, 1), datetime.min.time()).replace(tzinfo=timezone.utc)
255         end = datetime.combine(date(start.year + 1, 1, 1), datetime.min.time()).replace(tzinfo=timezone.utc)
256     elif end_year is None:
257         start = datetime.combine(date(start_year, 1, 1), datetime.min.time()).replace(tzinfo=timezone.utc)
258         end = datetime.combine(date(datetime.now().year, 1, 1), datetime.min.time()).replace(tzinfo=timezone.utc)
259     else:
260         start = datetime.combine(date(start_year, 1, 1), datetime.min.time()).replace(tzinfo=timezone.utc)
261         end = datetime.combine(date(end_year + 1, 1, 1), datetime.min.time()).replace(tzinfo=timezone.utc)
262
263     if (start.year == end.year - 1):
264         file_name = "yearly_booking_data_" + str(start.year) + ".csv"
265     else:
266         file_name = "yearly_booking_data_" + str(start.year) + "-" + str(end.year - 1) + ".csv"
267
268     with open(file_name, "w", newline="") as file:
269         file_writer = csv.writer(file)
270         cumulative_data = CumulativeData(file_writer)
271         file_writer.writerow(
272             [
273                 'ID',
274                 'Project',
275                 'Purpose',
276                 'User',
277                 'Collaborators',
278                 'Extensions Left',
279                 'Usage Days',
280                 'Start',
281                 'End'
282             ]
283         )
284
285         for booking in Booking.objects.filter(start__gte=start, start__lte=end):
286             filtered = False
287             booking_filter = [279]
288             user_filter = ["ParkerBerberian", "ssmith", "ahassick", "sbergeron", "jhodgdon", "rhodgdon", "aburch", "jspewock"]
289             user = booking.owner.username if booking.owner.username is not None else "None"
290
291             for b in booking_filter:
292                 if b == booking.id:
293                     filtered = True
294
295             for u in user_filter:
296                 if u == user:
297                     filtered = True
298             # trims time delta to the the specified year(s) if between years
299             usage_days = ((end if booking.end > end else booking.end) - (start if booking.start < start else booking.start)).days
300             collaborators = []
301
302             for c in booking.collaborators.all():
303                 collaborators.append(c.username)
304
305             if (not filtered):
306                 cumulative_data.account(booking, usage_days)
307                 file_writer.writerow([
308                     str(booking.id),
309                     str(booking.project),
310                     str(booking.purpose),
311                     str(booking.owner.username),
312                     ','.join(collaborators),
313                     str(booking.ext_count),
314                     str(usage_days),
315                     str(booking.start),
316                     str(booking.end)
317                 ])
318         cumulative_data.write_cumulative()
319
320
321 def map_cntt_interfaces(labid: str):
322     """
323     Use this during cntt migrations, call it with a host labid and it will change profiles for this host
324     as well as mapping its interfaces across. interface ens1f2 should have the mac address of interface eno50
325     as an invariant before calling this function
326     """
327     host = get_host(labid, "unh_iol")
328     host.profile = ResourceProfile.objects.get(name="HPE x86 CNTT")
329     host.save()
330     host = get_host(labid, "unh_iol")
331
332     for iface in host.interfaces.all():
333         new_ifprofile = None
334         if iface.profile.name == "ens1f2":
335             new_ifprofile = InterfaceProfile.objects.get(host=host.profile, name="eno50")
336         else:
337             new_ifprofile = InterfaceProfile.objects.get(host=host.profile, name=iface.profile.name)
338
339         iface.profile = new_ifprofile
340
341         iface.save()
342
343
344 def detect_leaked_hosts(labid="unh_iol"):
345     """
346     Use this to try to detect leaked hosts.
347     These hosts may still be in the process of unprovisioning,
348     but if they are not (or unprovisioning is frozen) then
349     these hosts are instead leaked
350     """
351     working_servers = Server.objects.filter(working=True, lab__lab_user__username=labid)
352     booked = working_servers.filter(booked=True)
353     filtered = booked
354     print_div()
355     print("In use now:")
356     for booking in Booking.objects.filter(end__gte=timezone.now()):
357         res_for_booking = booking.resource.get_resources()
358         print(res_for_booking)
359         for resource in res_for_booking:
360             filtered = filtered.exclude(id=resource.id)
361     print_div()
362     print("Possibly leaked:")
363     for host in filtered:
364         print(host)
365     print_div()
366     return filtered
367
368
369 def booking_for_host(host_labid: str, lab_username="unh_iol"):
370     """
371     Returns the booking that this server is a part of, if any.
372     Fails with an exception if no such booking exists
373
374     @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
375
376     @lab_username: param of the form `unh_iol` or similar
377     """
378     server = Server.objects.get(lab__lab_user__username=lab_username, labid=host_labid)
379     booking = server.bundle.booking_set.first()
380     print_div()
381     print(booking)
382     print("id:", booking.id)
383     print("owner:", booking.owner)
384     print("job (id):", booking.job, "(" + str(booking.job.id) + ")")
385     print_div()
386     return booking
387
388
389 def force_release_booking(booking_id: int):
390     """
391     Takes a booking id and forces the booking to end whether or not the tasks have
392     completed normally.
393
394     Use with caution! Hosts may or may not be released depending on other underlying issues
395
396     @booking_id: the id of the Booking object to be released
397     """
398     booking = Booking.objects.get(id=booking_id)
399     job = booking.job
400     tasks = job.get_tasklist()
401     for task in tasks:
402         task.status = JobStatus.DONE
403         task.save()
404
405
406 def free_leaked_public_vlans(safety_buffer_days=2):
407     for lab in Lab.objects.all():
408         current_booking_set = Booking.objects.filter(end__gte=timezone.now() + timedelta(days=safety_buffer_days))
409
410         marked_nets = set()
411
412         for booking in current_booking_set:
413             for network in get_network_metadata(booking.id):
414                 marked_nets.add(network["vlan_id"])
415
416         for net in PublicNetwork.objects.filter(lab=lab).filter(in_use=True):
417             if net.vlan not in marked_nets:
418                 lab.vlan_manager.release_public_vlan(net.vlan)
419
420
421 def get_network_metadata(booking_id: int):
422     """
423     Takes a booking id and prints all (known) networks that are owned by it.
424     Returns an object of the form {<network name>: {"vlan_id": int, "netname": str <network name>, "public": bool <whether network is public/routable}}
425
426     @booking_id: the id of the Booking object to be queried
427     """
428     booking = Booking.objects.get(id=booking_id)
429     bundle = booking.resource
430     pnets = PhysicalNetwork.objects.filter(bundle=bundle).all()
431     metadata = {}
432     for pnet in pnets:
433         net = pnet.generic_network
434         mdata = {"vlan_id": pnet.vlan_id, "netname": net.name, "public": net.is_public}
435         metadata[net.name] = mdata
436     return metadata
437
438
439 def print_dict_pretty(a_dict):
440     """
441     admin_utils internal function
442     """
443
444     print(json.dumps(a_dict, sort_keys=True, indent=4))
445
446
447 def import_host(filenames):
448     """
449     Imports host from an array of converted inspection files and if needed creates a new profile for the host.
450     NOTE: CONVERT INSPECTION FILES USING convert_inspect_results(["file", "file"])
451     (original file names not including "-import.yaml" i.e. hpe44) AND FILL IN <NEEDED FIELDS> BEFORE THIS
452     @filenames: array of host import file names to import
453     """
454
455     for filename in filenames:
456
457         # open import file
458         file = open("dashboard/" + filename + "-import.yaml", "r")
459         data = yaml.safe_load(file)
460
461         # if a new profile is needed create one and a matching template
462         if (data["new_profile"]):
463             add_profile(data)
464             print("Profile: " + data["name"] + " created!")
465             make_default_template(
466                 ResourceProfile.objects.get(name=data["name"]),
467                 Image.objects.get(lab_id=data["image"]).id,
468                 None,
469                 None,
470                 False,
471                 False,
472                 data["owner"],
473                 "unh_iol",
474                 True,
475                 False,
476                 data["temp_desc"]
477             )
478
479             print(" Template: " + data["temp_name"] + " created!")
480
481         # add the server
482         add_server(
483             ResourceProfile.objects.get(name=data["name"]),
484             data["hostname"],
485             data["interfaces"],
486             data["lab"],
487             data["vendor"],
488             data["model"]
489         )
490
491         print(data["hostname"] + " imported!")
492
493
494 def convert_inspect_results(files):
495     """
496     Converts an array of inspection result files into templates (filename-import.yaml) to be filled out for importing the servers into the dashboard
497     @files an array of file names (not including the file type. i.e hpe44). Default: []
498     """
499     for filename in files:
500         # open host inspect file
501         file = open("dashboard/" + filename + ".yaml")
502         output = open("dashboard/" + filename + "-import.yaml", "w")
503         data = json.load(file)
504
505         # gather data about disks
506         disk_data = {}
507         for i in data["disk"]:
508
509             # don't include loops in disks
510             if "loop" not in i:
511                 disk_data[i["name"]] = {
512                     "capacity": i["size"][:-3],
513                     "media_type": "<\"SSD\" or \"HDD\">",
514                     "interface": "<\"sata\", \"sas\", \"ssd\", \"nvme\", \"scsi\", or \"iscsi\">",
515                 }
516
517         # gather interface data
518         interface_data = {}
519         for i in data["interfaces"]:
520             interface_data[data["interfaces"][i]["name"]] = {
521                 "speed": data["interfaces"][i]["speed"],
522                 "nic_type": "<\"onboard\" or \"pcie\">",
523                 "order": "<order in switch>",
524                 "mac_address": data["interfaces"][i]["mac"],
525                 "bus_addr": data["interfaces"][i]["busaddr"],
526             }
527
528         # gather cpu data
529         cpu_data = {
530             "cores": data["cpu"]["cores"],
531             "architecture": data["cpu"]["arch"],
532             "cpus": data["cpu"]["cpus"],
533             "cflags": "<cflags string>",
534         }
535
536         # gather ram data
537         ram_data = {
538             "amount": data["memory"][:-1],
539             "channels": "<int of ram channels used>",
540         }
541
542         # assemble data for host import file
543         import_data = {
544             "new_profile": "<True or False> (Set to True to create a new profile for the host's type)",
545             "name": "<profile name> (Used to set the profile of a host and for creating a new profile)",
546             "description": "<profile description>",
547             "labs": "<labs using profile>",
548             "temp_name": "<Template name>",
549             "temp_desc": "<template description>",
550             "image": "<image lab_id>",
551             "owner": "<template owner>",
552             "hostname": data["hostname"],
553             "lab": "<lab server is in> (i.e. \"unh_iol\")",
554             "disks": disk_data,
555             "interfaces": interface_data,
556             "cpus": cpu_data,
557             "ram": ram_data,
558             "vendor": "<host vendor>",
559             "model": "<host model>",
560         }
561
562         # export data as yaml
563         yaml.dump(import_data, output)
564
565
566 def add_profile(data):
567     """
568     Used for adding a host profile to the dashboard
569
570     schema (of dict passed as "data" param):
571     {
572         "name": str
573         "description": str
574         "labs": [
575             str (lab username)
576         ]
577         "disks": {
578             <diskname> : {
579                 capacity: int (GiB)
580                 media_type: str ("SSD" or "HDD")
581                 interface: str ("sata", "sas", "ssd", "nvme", "scsi", or "iscsi")
582             }
583         }
584         interfaces: {
585             <intname>: {
586                 "speed": int (mbit)
587                 "nic_type": str ("onboard" or "pcie")
588                 "order": int (compared to the other interfaces, indicates the "order" that the ports are laid out)
589             }
590         }
591         cpus: {
592             cores: int (hardware threads count)
593             architecture: str (x86_64" or "aarch64")
594             cpus: int (number of sockets)
595             cflags: str
596         }
597         ram: {
598             amount: int (GiB)
599             channels: int
600         }
601     }
602     """
603     base_profile = ResourceProfile.objects.create(name=data['name'], description=data['description'])
604     base_profile.save()
605
606     for lab_username in data['labs']:
607         lab = Lab.objects.get(lab_user__username=lab_username)
608
609         base_profile.labs.add(lab)
610         base_profile.save()
611
612     for diskname in data['disks'].keys():
613         disk = data['disks'][diskname]
614
615         disk_profile = DiskProfile.objects.create(name=diskname, size=disk['capacity'], media_type=disk['media_type'], interface=disk['interface'], host=base_profile)
616         disk_profile.save()
617
618     for ifacename in data['interfaces'].keys():
619         iface = data['interfaces'][ifacename]
620
621         iface_profile = InterfaceProfile.objects.create(name=ifacename, speed=iface['speed'], nic_type=iface['nic_type'], order=iface['order'], host=base_profile)
622         iface_profile.save()
623
624     cpu = data['cpus']
625     cpu_prof = CpuProfile.objects.create(cores=cpu['cores'], architecture=cpu['architecture'], cpus=cpu['cpus'], cflags=cpu['cflags'], host=base_profile)
626     cpu_prof.save()
627
628     ram_prof = RamProfile.objects.create(amount=data['ram']['amount'], channels=data['ram']['channels'], host=base_profile)
629     ram_prof.save()
630
631
632 def make_default_template(resource_profile, image_id=None, template_name=None, connected_interface_names=None, interfaces_tagged=False, connected_interface_tagged=False, owner_username="root", lab_username="unh_iol", public=True, temporary=False, description=""):
633     """
634     Do not call this function without reading the related source code, it may have unintended effects.
635
636     Used for creating a default template from some host profile
637     """
638
639     if not resource_profile:
640         raise Exception("No viable continuation from none resource_profile")
641
642     if not template_name:
643         template_name = resource_profile.name
644
645     if not connected_interface_names:
646         connected_interface_names = [InterfaceProfile.objects.filter(host=resource_profile).first().name]
647         print("setting connected interface names to", connected_interface_names)
648
649     if not image_id:
650         image_id = Image.objects.filter(host_type=resource_profile).first().id
651
652     image = Image.objects.get(id=image_id)
653
654     base = ResourceTemplate.objects.create(
655         name=template_name,
656         xml="",
657         owner=User.objects.get(username=owner_username),
658         lab=Lab.objects.get(lab_user__username=lab_username), description=description,
659         public=public, temporary=temporary, copy_of=None)
660
661     rconf = ResourceConfiguration.objects.create(profile=resource_profile, image=image, template=base, is_head_node=True, name="opnfv_host")
662     rconf.save()
663
664     connected_interfaces = []
665
666     for iface_prof in InterfaceProfile.objects.filter(host=resource_profile).all():
667         iface_conf = InterfaceConfiguration.objects.create(profile=iface_prof, resource_config=rconf)
668
669         if iface_prof.name in connected_interface_names:
670             connected_interfaces.append(iface_conf)
671
672     network = Network.objects.create(name="public", bundle=base, is_public=True)
673
674     for iface in connected_interfaces:
675         connection = NetworkConnection.objects.create(network=network, vlan_is_tagged=interfaces_tagged)
676         connection.save()
677
678         iface.connections.add(connection)
679         print("adding connection to iface ", iface)
680         iface.save()
681         connection.save()
682
683
684 def add_server(profile, name, interfaces, lab_username="unh_iol", vendor="unknown", model="unknown"):
685     """
686     Used to enroll a new host of some profile
687
688     @profile: the ResourceProfile in question (by reference to a model object)
689
690     @name: the unique name of the server, currently indistinct from labid
691
692     @interfaces: interfaces should be dict from interface name (eg ens1f0) to dict of schema:
693         {
694             mac_address: <mac addr>,
695             bus_addr: <bus addr>, //this field is optional, "" is default
696         }
697
698     @lab_username: username of the lab to be added to
699
700     @vendor: vendor name of the host, such as "HPE" or "Gigabyte"
701
702     @model: specific model of the host, such as "DL380 Gen 9"
703
704     """
705     server = Server.objects.create(
706         bundle=None,
707         profile=profile,
708         config=None,
709         working=True,
710         vendor=vendor,
711         model=model,
712         labid=name,
713         lab=Lab.objects.get(lab_user__username=lab_username),
714         name=name,
715         booked=False)
716
717     for iface_prof in InterfaceProfile.objects.filter(host=profile).all():
718         mac_addr = interfaces[iface_prof.name]["mac_address"]
719         bus_addr = "unknown"
720         if "bus_addr" in interfaces[iface_prof.name].keys():
721             bus_addr = interfaces[iface_prof.name]["bus_addr"]
722
723         iface = Interface.objects.create(acts_as=None, profile=iface_prof, mac_address=mac_addr, bus_address=bus_addr)
724         iface.save()
725
726         server.interfaces.add(iface)
727         server.save()
728
729
730 def extend_booking(booking_id, days=0, hours=0, minutes=0, weeks=0):
731     """
732     Extend a booking by n <days, hours, minutes, weeks>
733
734     @booking_id: id of the booking
735
736     @days/@hours/@minutes/@weeks: the cumulative amount of delta to add to the length of the booking
737     """
738
739     booking = Booking.objects.get(id=booking_id)
740     booking.end = booking.end + timedelta(days=days, hours=hours, minutes=minutes, weeks=weeks)
741     booking.save()
742
743
744 def regenerate_cloud_configs(booking_id):
745     b = Booking.objects.get(id=booking_id)
746     for res in b.resource.get_resources():
747         res.config.cloud_init_files.set(res.config.cloud_init_files.filter(generated=False))  # careful!
748         res.config.save()
749         cif = GeneratedCloudConfig.objects.create(resource_id=res.labid, booking=b, rconfig=res.config)
750         cif.save()
751         cif = CloudInitFile.create(priority=0, text=cif.serialize())
752         cif.save()
753         res.config.cloud_init_files.add(cif)
754         res.config.save()
755
756
757 def set_job_new(job_id):
758     j = Job.objects.get(id=job_id)
759     b = j.booking
760     regenerate_cloud_configs(b.id)
761     for task in j.get_tasklist():
762         task.status = JobStatus.NEW
763         task.save()
764     j.status = JobStatus.NEW
765     j.save()
766
767
768 def docs(function=None, fulltext=False):
769     """
770     Print documentation for a given function in admin_utils.
771     Call without arguments for more information
772     """
773
774     fn = None
775
776     if isinstance(function, str):
777         try:
778             fn = globals()[function]
779         except KeyError:
780             print("Couldn't find a function by the given name")
781             return
782     elif callable(function):
783         fn = function
784     else:
785         print("docs(function: callable | str, fulltext: bool) was called with a 'function' that was neither callable nor a string name of a function")
786         print("usage: docs('some_function_in_admin_utils', fulltext=True)")
787         print("The 'fulltext' argument is used to choose if you want the complete source of the function printed. If this argument is false then you will only see the pydoc rendered documentation for the function")
788         return
789
790     if not fn:
791         print("couldn't find a function by that name")
792
793     if not fulltext:
794         print("Pydoc documents the function as such:")
795         print(pydoc.render_doc(fn))
796     else:
797         print("The full source of the function is this:")
798         print(inspect.getsource(fn))
799
800
801 def admin_functions():
802     """
803     List functions available to call within admin_utils
804     """
805
806     return [name for name, func in inspect.getmembers(sys.modules[__name__]) if (inspect.isfunction(func) and func.__module__ == __name__)]
807
808
809 print("Hint: call `docs(<function name>)` or `admin_functions()` for help on using the admin utils")
810 print("docs(<function name>) displays documentation on a given function")
811 print("admin_functions() lists all functions available to call within this module")