045caeb9147e7eb7eb3de8bb04d05d722130639a
[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 sys
31 import inspect
32 import pydoc
33
34 from django.contrib.auth.models import User
35
36 from account.models import (
37     Lab,
38     PublicNetwork
39 )
40
41 from resource_inventory.resource_manager import ResourceManager
42 from resource_inventory.pdf_templater import PDFTemplater
43
44 from booking.quick_deployer import update_template
45
46 from datetime import timedelta
47
48 from django.utils import 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 def map_cntt_interfaces(labid: str):
229     """
230     Use this during cntt migrations, call it with a host labid and it will change profiles for this host
231     as well as mapping its interfaces across. interface ens1f2 should have the mac address of interface eno50
232     as an invariant before calling this function
233     """
234     host = get_host(labid, "unh_iol")
235     host.profile = ResourceProfile.objects.get(name="HPE x86 CNTT")
236     host.save()
237     host = get_host(labid, "unh_iol")
238
239     for iface in host.interfaces.all():
240         new_ifprofile = None
241         if iface.profile.name == "ens1f2":
242             new_ifprofile = InterfaceProfile.objects.get(host=host.profile, name="eno50")
243         else:
244             new_ifprofile = InterfaceProfile.objects.get(host=host.profile, name=iface.profile.name)
245
246         iface.profile = new_ifprofile
247
248         iface.save()
249
250
251 def detect_leaked_hosts(labid="unh_iol"):
252     """
253     Use this to try to detect leaked hosts.
254     These hosts may still be in the process of unprovisioning,
255     but if they are not (or unprovisioning is frozen) then
256     these hosts are instead leaked
257     """
258     working_servers = Server.objects.filter(working=True, lab__lab_user__username=labid)
259     booked = working_servers.filter(booked=True)
260     filtered = booked
261     print_div()
262     print("In use now:")
263     for booking in Booking.objects.filter(end__gte=timezone.now()):
264         res_for_booking = booking.resource.get_resources()
265         print(res_for_booking)
266         for resource in res_for_booking:
267             filtered = filtered.exclude(id=resource.id)
268     print_div()
269     print("Possibly leaked:")
270     for host in filtered:
271         print(host)
272     print_div()
273     return filtered
274
275
276 def booking_for_host(host_labid: str, lab_username="unh_iol"):
277     """
278     Returns the booking that this server is a part of, if any.
279     Fails with an exception if no such booking exists
280
281     @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
282
283     @lab_username: param of the form `unh_iol` or similar
284     """
285     server = Server.objects.get(lab__lab_user__username=lab_username, labid=host_labid)
286     booking = server.bundle.booking_set.first()
287     print_div()
288     print(booking)
289     print("id:", booking.id)
290     print("owner:", booking.owner)
291     print("job (id):", booking.job, "(" + str(booking.job.id) + ")")
292     print_div()
293     return booking
294
295
296 def force_release_booking(booking_id: int):
297     """
298     Takes a booking id and forces the booking to end whether or not the tasks have
299     completed normally.
300
301     Use with caution! Hosts may or may not be released depending on other underlying issues
302
303     @booking_id: the id of the Booking object to be released
304     """
305     booking = Booking.objects.get(id=booking_id)
306     job = booking.job
307     tasks = job.get_tasklist()
308     for task in tasks:
309         task.status = JobStatus.DONE
310         task.save()
311
312
313 def free_leaked_public_vlans(safety_buffer_days=2):
314     for lab in Lab.objects.all():
315         current_booking_set = Booking.objects.filter(end__gte=timezone.now() + timedelta(days=safety_buffer_days))
316
317         marked_nets = set()
318
319         for booking in current_booking_set:
320             for network in get_network_metadata(booking.id):
321                 marked_nets.add(network["vlan_id"])
322
323         for net in PublicNetwork.objects.filter(lab=lab).filter(in_use=True):
324             if net.vlan not in marked_nets:
325                 lab.vlan_manager.release_public_vlan(net.vlan)
326
327
328 def get_network_metadata(booking_id: int):
329     """
330     Takes a booking id and prints all (known) networks that are owned by it.
331     Returns an object of the form {<network name>: {"vlan_id": int, "netname": str <network name>, "public": bool <whether network is public/routable}}
332
333     @booking_id: the id of the Booking object to be queried
334     """
335     booking = Booking.objects.get(id=booking_id)
336     bundle = booking.resource
337     pnets = PhysicalNetwork.objects.filter(bundle=bundle).all()
338     metadata = {}
339     for pnet in pnets:
340         net = pnet.generic_network
341         mdata = {"vlan_id": pnet.vlan_id, "netname": net.name, "public": net.is_public}
342         metadata[net.name] = mdata
343     return metadata
344
345
346 def print_dict_pretty(a_dict):
347     """
348     admin_utils internal function
349     """
350
351     print(json.dumps(a_dict, sort_keys=True, indent=4))
352
353
354 def add_profile(data):
355     """
356     Used for adding a host profile to the dashboard
357
358     schema (of dict passed as "data" param):
359     {
360         "name": str
361         "description": str
362         "labs": [
363             str (lab username)
364         ]
365         "disks": {
366             <diskname> : {
367                 capacity: int (GiB)
368                 media_type: str ("SSD" or "HDD")
369                 interface: str ("sata", "sas", "ssd", "nvme", "scsi", or "iscsi")
370             }
371         }
372         interfaces: {
373             <intname>: {
374                 "speed": int (mbit)
375                 "nic_type": str ("onboard" or "pcie")
376                 "order": int (compared to the other interfaces, indicates the "order" that the ports are laid out)
377             }
378         }
379         cpus: {
380             cores: int (hardware threads count)
381             architecture: str (x86_64" or "aarch64")
382             cpus: int (number of sockets)
383             cflags: str
384         }
385         ram: {
386             amount: int (GiB)
387             channels: int
388         }
389     }
390     """
391     base_profile = ResourceProfile.objects.create(name=data['name'], description=data['description'])
392     base_profile.save()
393
394     for lab_username in data['labs']:
395         lab = Lab.objects.get(lab_user__username=lab_username)
396
397         base_profile.labs.add(lab)
398         base_profile.save()
399
400     for diskname in data['disks'].keys():
401         disk = data['disks'][diskname]
402
403         disk_profile = DiskProfile.objects.create(name=diskname, size=disk['capacity'], media_type=disk['media_type'], interface=disk['interface'], host=base_profile)
404         disk_profile.save()
405
406     for ifacename in data['interfaces'].keys():
407         iface = data['interfaces'][ifacename]
408
409         iface_profile = InterfaceProfile.objects.create(name=ifacename, speed=iface['speed'], nic_type=iface['nic_type'], order=iface['order'], host=base_profile)
410         iface_profile.save()
411
412     cpu = data['cpus']
413     cpu_prof = CpuProfile.objects.create(cores=cpu['cores'], architecture=cpu['architecture'], cpus=cpu['cpus'], cflags=cpu['cflags'], host=base_profile)
414     cpu_prof.save()
415
416     ram_prof = RamProfile.objects.create(amount=data['ram']['amount'], channels=data['ram']['channels'], host=base_profile)
417     ram_prof.save()
418
419
420 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=""):
421     """
422     Do not call this function without reading the related source code, it may have unintended effects.
423
424     Used for creating a default template from some host profile
425     """
426
427     if not resource_profile:
428         raise Exception("No viable continuation from none resource_profile")
429
430     if not template_name:
431         template_name = resource_profile.name
432
433     if not connected_interface_names:
434         connected_interface_names = [InterfaceProfile.objects.filter(host=resource_profile).first().name]
435         print("setting connected interface names to", connected_interface_names)
436
437     if not image_id:
438         image_id = Image.objects.filter(host_type=resource_profile).first().id
439
440     image = Image.objects.get(id=image_id)
441
442     base = ResourceTemplate.objects.create(
443         name=template_name,
444         xml="",
445         owner=User.objects.get(username=owner_username),
446         lab=Lab.objects.get(lab_user__username=lab_username), description=description,
447         public=public, temporary=temporary, copy_of=None)
448
449     rconf = ResourceConfiguration.objects.create(profile=resource_profile, image=image, template=base, is_head_node=True, name="opnfv_host")
450     rconf.save()
451
452     connected_interfaces = []
453
454     for iface_prof in InterfaceProfile.objects.filter(host=resource_profile).all():
455         iface_conf = InterfaceConfiguration.objects.create(profile=iface_prof, resource_config=rconf)
456
457         if iface_prof.name in connected_interface_names:
458             connected_interfaces.append(iface_conf)
459
460     network = Network.objects.create(name="public", bundle=base, is_public=True)
461
462     for iface in connected_interfaces:
463         connection = NetworkConnection.objects.create(network=network, vlan_is_tagged=interfaces_tagged)
464         connection.save()
465
466         iface.connections.add(connection)
467         print("adding connection to iface ", iface)
468         iface.save()
469         connection.save()
470
471
472 def add_server(profile, name, interfaces, lab_username="unh_iol", vendor="unknown", model="unknown"):
473     """
474     Used to enroll a new host of some profile
475
476     @profile: the ResourceProfile in question (by reference to a model object)
477
478     @name: the unique name of the server, currently indistinct from labid
479
480     @interfaces: interfaces should be dict from interface name (eg ens1f0) to dict of schema:
481         {
482             mac_address: <mac addr>,
483             bus_addr: <bus addr>, //this field is optional, "" is default
484         }
485
486     @lab_username: username of the lab to be added to
487
488     @vendor: vendor name of the host, such as "HPE" or "Gigabyte"
489
490     @model: specific model of the host, such as "DL380 Gen 9"
491
492     """
493     server = Server.objects.create(
494         bundle=None,
495         profile=profile,
496         config=None,
497         working=True,
498         vendor=vendor,
499         model=model,
500         labid=name,
501         lab=Lab.objects.get(lab_user__username=lab_username),
502         name=name,
503         booked=False)
504
505     for iface_prof in InterfaceProfile.objects.filter(host=profile).all():
506         mac_addr = interfaces[iface_prof.name]["mac_address"]
507         bus_addr = "unknown"
508         if "bus_addr" in interfaces[iface_prof.name].keys():
509             bus_addr = interfaces[iface_prof.name]["bus_addr"]
510
511         iface = Interface.objects.create(acts_as=None, profile=iface_prof, mac_address=mac_addr, bus_address=bus_addr)
512         iface.save()
513
514         server.interfaces.add(iface)
515         server.save()
516
517
518 def extend_booking(booking_id, days=0, hours=0, minutes=0, weeks=0):
519     """
520     Extend a booking by n <days, hours, minutes, weeks>
521
522     @booking_id: id of the booking
523
524     @days/@hours/@minutes/@weeks: the cumulative amount of delta to add to the length of the booking
525     """
526
527     booking = Booking.objects.get(id=booking_id)
528     booking.end = booking.end + timedelta(days=days, hours=hours, minutes=minutes, weeks=weeks)
529     booking.save()
530
531
532 def regenerate_cloud_configs(booking_id):
533     b = Booking.objects.get(id=booking_id)
534     for res in b.resource.get_resources():
535         res.config.cloud_init_files.set(res.config.cloud_init_files.filter(generated=False))  # careful!
536         res.config.save()
537         cif = GeneratedCloudConfig.objects.create(resource_id=res.labid, booking=b, rconfig=res.config)
538         cif.save()
539         cif = CloudInitFile.create(priority=0, text=cif.serialize())
540         cif.save()
541         res.config.cloud_init_files.add(cif)
542         res.config.save()
543
544
545 def set_job_new(job_id):
546     j = Job.objects.get(id=job_id)
547     b = j.booking
548     regenerate_cloud_configs(b.id)
549     for task in j.get_tasklist():
550         task.status = JobStatus.NEW
551         task.save()
552     j.status = JobStatus.NEW
553     j.save()
554
555
556 def docs(function=None, fulltext=False):
557     """
558     Print documentation for a given function in admin_utils.
559     Call without arguments for more information
560     """
561
562     fn = None
563
564     if isinstance(function, str):
565         try:
566             fn = globals()[function]
567         except KeyError:
568             print("Couldn't find a function by the given name")
569             return
570     elif callable(function):
571         fn = function
572     else:
573         print("docs(function: callable | str, fulltext: bool) was called with a 'function' that was neither callable nor a string name of a function")
574         print("usage: docs('some_function_in_admin_utils', fulltext=True)")
575         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")
576         return
577
578     if not fn:
579         print("couldn't find a function by that name")
580
581     if not fulltext:
582         print("Pydoc documents the function as such:")
583         print(pydoc.render_doc(fn))
584     else:
585         print("The full source of the function is this:")
586         print(inspect.getsource(fn))
587
588
589 def admin_functions():
590     """
591     List functions available to call within admin_utils
592     """
593
594     return [name for name, func in inspect.getmembers(sys.modules[__name__]) if (inspect.isfunction(func) and func.__module__ == __name__)]
595
596
597 print("Hint: call `docs(<function name>)` or `admin_functions()` for help on using the admin utils")
598 print("docs(<function name>) displays documentation on a given function")
599 print("admin_functions() lists all functions available to call within this module")