Merge master for RC
[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 )
27
28 import json
29 import sys
30 import inspect
31 import pydoc
32
33 from django.contrib.auth.models import User
34
35 from account.models import (
36     Lab,
37     PublicNetwork
38 )
39
40 from resource_inventory.resource_manager import ResourceManager
41 from resource_inventory.pdf_templater import PDFTemplater
42
43 from booking.quick_deployer import update_template
44
45 from datetime import timedelta
46
47 from django.utils import timezone
48
49 from booking.models import Booking
50 from notifier.manager import NotificationHandler
51 from api.models import JobFactory
52
53 from api.models import JobStatus
54
55
56 def print_div():
57     """
58     Utility function for printing dividers, does nothing directly useful as a utility
59     """
60     print("=" * 68)
61
62
63 def book_host(owner_username, host_labid, lab_username, hostname, image_id, template_name, length_days=21, collaborator_usernames=[], purpose="internal", project="LaaS"):
64     """
65     creates a quick booking using the given host
66
67     @owner_username is the simple username for the user who will own the resulting booking.
68     Do not set this to a lab username!
69
70     @image_id is the django id of the image in question, NOT the labid of the image.
71     Query Image objects by their public status and compatible host types
72
73     @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
74
75     @lab_username for iol is `unh_iol`, other labs will be documented here
76
77     @hostname the hostname that the resulting host should have set
78
79     @template_name the name of the (public, or user accessible) template to use for this booking
80
81     @length_days how long the booking should be, no hard limit currently
82
83     @collaborator_usernames a list of usernames for collaborators to the booking
84
85     @purpose what this booking will be used for
86
87     @project what project/group this booking is on behalf of or the owner represents
88     """
89     lab = Lab.objects.get(lab_user__username=lab_username)
90     host = Server.objects.filter(lab=lab).get(labid=host_labid)
91     if host.booked:
92         print("Can't book host, already marked as booked")
93         return
94     else:
95         host.booked = True
96         host.save()
97
98     template = ResourceTemplate.objects.filter(public=True).get(name=template_name)
99     image = Image.objects.get(id=image_id)
100
101     owner = User.objects.get(username=owner_username)
102
103     new_template = update_template(template, image, hostname, owner)
104
105     rmanager = ResourceManager.getInstance()
106
107     vlan_map = rmanager.get_vlans(new_template)
108
109     # only a single host so can reuse var for iter here
110     resource_bundle = ResourceBundle.objects.create(template=new_template)
111     res_configs = new_template.getConfigs()
112
113     for config in res_configs:
114         try:
115             host.bundle = resource_bundle
116             host.config = config
117             rmanager.configureNetworking(resource_bundle, host, vlan_map)
118             host.save()
119         except Exception:
120             host.booked = False
121             host.save()
122             print("Failed to book host due to error configuring it")
123             return
124
125     new_template.save()
126
127     booking = Booking.objects.create(
128         purpose=purpose,
129         project=project,
130         lab=lab,
131         owner=owner,
132         start=timezone.now(),
133         end=timezone.now() + timedelta(days=int(length_days)),
134         resource=resource_bundle,
135         opnfv_config=None
136     )
137
138     booking.pdf = PDFTemplater.makePDF(booking)
139
140     booking.save()
141
142     for collaborator_username in collaborator_usernames:
143         try:
144             user = User.objects.get(username=collaborator_username)
145             booking.collaborators.add(user)
146         except Exception:
147             print("couldn't add user with username ", collaborator_username)
148
149     booking.save()
150
151     JobFactory.makeCompleteJob(booking)
152     NotificationHandler.notify_new_booking(booking)
153
154
155 def mark_working(host_labid, lab_username, working=True):
156     """
157     Mark a host working/not working so that it is either bookable or hidden in the dashboard.
158
159     @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
160
161     @lab_username: param of the form `unh_iol` or similar
162
163     @working: bool, whether by the end of execution the host should be considered working or not working
164     """
165
166     lab = Lab.objects.get(lab_user__username=lab_username)
167     server = Server.objects.filter(lab=lab).get(labid=host_labid)
168     print("changing server working status from ", server.working, "to", working)
169     server.working = working
170     server.save()
171
172
173 def mark_booked(host_labid, lab_username, booked=True):
174     """
175     Mark a host as booked/unbooked
176
177     @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
178
179     @lab_username: param of the form `unh_iol` or similar
180
181     @working: bool, whether by the end of execution the host should be considered booked or not booked
182     """
183
184     lab = Lab.objects.get(lab_user__username=lab_username)
185     server = Server.objects.filter(lab=lab).get(labid=host_labid)
186     print("changing server booked status from ", server.booked, "to", booked)
187     server.booked = booked
188     server.save()
189
190
191 def get_host(host_labid, lab_username):
192     """
193     Returns host filtered by lab and then unique id within lab
194
195     @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
196
197     @lab_username: param of the form `unh_iol` or similar
198     """
199     lab = Lab.objects.get(lab_user__username=lab_username)
200     return Server.objects.filter(lab=lab).get(labid=host_labid)
201
202
203 def get_info(host_labid, lab_username):
204     """
205     Returns various information on the host queried by the given parameters
206
207     @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
208
209     @lab_username: param of the form `unh_iol` or similar
210     """
211     info = {}
212     host = get_host(host_labid, lab_username)
213     info['host_labid'] = host_labid
214     info['booked'] = host.booked
215     info['working'] = host.working
216     info['profile'] = str(host.profile)
217     if host.bundle:
218         binfo = {}
219         info['bundle'] = binfo
220     if host.config:
221         cinfo = {}
222         info['config'] = cinfo
223
224     return info
225
226
227 def map_cntt_interfaces(labid: str):
228     """
229     Use this during cntt migrations, call it with a host labid and it will change profiles for this host
230     as well as mapping its interfaces across. interface ens1f2 should have the mac address of interface eno50
231     as an invariant before calling this function
232     """
233     host = get_host(labid, "unh_iol")
234     host.profile = ResourceProfile.objects.get(name="HPE x86 CNTT")
235     host.save()
236     host = get_host(labid, "unh_iol")
237
238     for iface in host.interfaces.all():
239         new_ifprofile = None
240         if iface.profile.name == "ens1f2":
241             new_ifprofile = InterfaceProfile.objects.get(host=host.profile, name="eno50")
242         else:
243             new_ifprofile = InterfaceProfile.objects.get(host=host.profile, name=iface.profile.name)
244
245         iface.profile = new_ifprofile
246
247         iface.save()
248
249
250 def detect_leaked_hosts(labid="unh_iol"):
251     """
252     Use this to try to detect leaked hosts.
253     These hosts may still be in the process of unprovisioning,
254     but if they are not (or unprovisioning is frozen) then
255     these hosts are instead leaked
256     """
257     working_servers = Server.objects.filter(working=True, lab__lab_user__username=labid)
258     booked = working_servers.filter(booked=True)
259     filtered = booked
260     print_div()
261     print("In use now:")
262     for booking in Booking.objects.filter(end__gte=timezone.now()):
263         res_for_booking = booking.resource.get_resources()
264         print(res_for_booking)
265         for resource in res_for_booking:
266             filtered = filtered.exclude(id=resource.id)
267     print_div()
268     print("Possibly leaked:")
269     for host in filtered:
270         print(host)
271     print_div()
272     return filtered
273
274
275 def booking_for_host(host_labid: str, lab_username="unh_iol"):
276     """
277     Returns the booking that this server is a part of, if any.
278     Fails with an exception if no such booking exists
279
280     @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
281
282     @lab_username: param of the form `unh_iol` or similar
283     """
284     server = Server.objects.get(lab__lab_user__username=lab_username, lab_username=host_labid)
285     booking = server.bundle.booking_set.first()
286     print_div()
287     print(booking)
288     print("id:", booking.id)
289     print("owner:", booking.owner)
290     print("job (id):", booking.job, "(" + str(booking.job.id) + ")")
291     print_div()
292     return booking
293
294
295 def force_release_booking(booking_id: int):
296     """
297     Takes a booking id and forces the booking to end whether or not the tasks have
298     completed normally.
299
300     Use with caution! Hosts may or may not be released depending on other underlying issues
301
302     @booking_id: the id of the Booking object to be released
303     """
304     booking = Booking.objects.get(id=booking_id)
305     job = booking.job
306     tasks = job.get_tasklist()
307     for task in tasks:
308         task.status = JobStatus.DONE
309         task.save()
310
311
312 def free_leaked_public_vlans(safety_buffer_days=2):
313     for lab in Lab.objects.all():
314         current_booking_set = Booking.objects.filter(end__gte=timezone.now() + timedelta(days=safety_buffer_days))
315
316         marked_nets = set()
317
318         for booking in current_booking_set:
319             for network in get_network_metadata(booking.id):
320                 marked_nets.add(network["vlan_id"])
321
322         for net in PublicNetwork.objects.filter(lab=lab).filter(in_use=True):
323             if net.vlan not in marked_nets:
324                 lab.vlan_manager.release_public_vlan(net.vlan)
325
326
327 def get_network_metadata(booking_id: int):
328     """
329     Takes a booking id and prints all (known) networks that are owned by it.
330     Returns an object of the form {<network name>: {"vlan_id": int, "netname": str <network name>, "public": bool <whether network is public/routable}}
331
332     @booking_id: the id of the Booking object to be queried
333     """
334     booking = Booking.objects.get(id=booking_id)
335     bundle = booking.resource
336     pnets = PhysicalNetwork.objects.filter(bundle=bundle).all()
337     metadata = {}
338     for pnet in pnets:
339         net = pnet.generic_network
340         mdata = {"vlan_id": pnet.vlan_id, "netname": net.name, "public": net.is_public}
341         metadata[net.name] = mdata
342     return metadata
343
344
345 def print_dict_pretty(a_dict):
346     """
347     admin_utils internal function
348     """
349
350     print(json.dumps(a_dict, sort_keys=True, indent=4))
351
352
353 def add_profile(data):
354     """
355     Used for adding a host profile to the dashboard
356
357     schema (of dict passed as "data" param):
358     {
359         "name": str
360         "description": str
361         "labs": [
362             str (lab username)
363         ]
364         "disks": {
365             <diskname> : {
366                 capacity: int (GiB)
367                 media_type: str ("SSD" or "HDD")
368                 interface: str ("sata", "sas", "ssd", "nvme", "scsi", or "iscsi")
369             }
370         }
371         interfaces: {
372             <intname>: {
373                 "speed": int (mbit)
374                 "nic_type": str ("onboard" or "pcie")
375                 "order": int (compared to the other interfaces, indicates the "order" that the ports are laid out)
376             }
377         }
378         cpus: {
379             cores: int (hardware threads count)
380             architecture: str (x86_64" or "aarch64")
381             cpus: int (number of sockets)
382             cflags: str
383         }
384         ram: {
385             amount: int (GiB)
386             channels: int
387         }
388     }
389     """
390     base_profile = ResourceProfile.objects.create(name=data['name'], description=data['description'])
391     base_profile.save()
392
393     for lab_username in data['labs']:
394         lab = Lab.objects.get(lab_user__username=lab_username)
395
396         base_profile.labs.add(lab)
397         base_profile.save()
398
399     for diskname in data['disks'].keys():
400         disk = data['disks'][diskname]
401
402         disk_profile = DiskProfile.objects.create(name=diskname, size=disk['capacity'], media_type=disk['media_type'], interface=disk['interface'], host=base_profile)
403         disk_profile.save()
404
405     for ifacename in data['interfaces'].keys():
406         iface = data['interfaces'][ifacename]
407
408         iface_profile = InterfaceProfile.objects.create(name=ifacename, speed=iface['speed'], nic_type=iface['nic_type'], order=iface['order'], host=base_profile)
409         iface_profile.save()
410
411     cpu = data['cpus']
412     cpu_prof = CpuProfile.objects.create(cores=cpu['cores'], architecture=cpu['architecture'], cpus=cpu['cpus'], cflags=cpu['cflags'], host=base_profile)
413     cpu_prof.save()
414
415     ram_prof = RamProfile.objects.create(amount=data['ram']['amount'], channels=data['ram']['channels'], host=base_profile)
416     ram_prof.save()
417
418
419 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=""):
420     """
421     Do not call this function without reading the related source code, it may have unintended effects.
422
423     Used for creating a default template from some host profile
424     """
425
426     if not resource_profile:
427         raise Exception("No viable continuation from none resource_profile")
428
429     if not template_name:
430         template_name = resource_profile.name
431
432     if not connected_interface_names:
433         connected_interface_names = [InterfaceProfile.objects.filter(host=resource_profile).first().name]
434         print("setting connected interface names to", connected_interface_names)
435
436     if not image_id:
437         image_id = Image.objects.filter(host_type=resource_profile).first().id
438
439     image = Image.objects.get(id=image_id)
440
441     base = ResourceTemplate.objects.create(
442         name=template_name,
443         xml="",
444         owner=User.objects.get(username=owner_username),
445         lab=Lab.objects.get(lab_user__username=lab_username), description=description,
446         public=public, temporary=temporary, copy_of=None)
447
448     rconf = ResourceConfiguration.objects.create(profile=resource_profile, image=image, template=base, is_head_node=True, name="opnfv_host")
449     rconf.save()
450
451     connected_interfaces = []
452
453     for iface_prof in InterfaceProfile.objects.filter(host=resource_profile).all():
454         iface_conf = InterfaceConfiguration.objects.create(profile=iface_prof, resource_config=rconf)
455
456         if iface_prof.name in connected_interface_names:
457             connected_interfaces.append(iface_conf)
458
459     network = Network.objects.create(name="public", bundle=base, is_public=True)
460
461     for iface in connected_interfaces:
462         connection = NetworkConnection.objects.create(network=network, vlan_is_tagged=interfaces_tagged)
463         connection.save()
464
465         iface.connections.add(connection)
466         print("adding connection to iface ", iface)
467         iface.save()
468         connection.save()
469
470
471 def add_server(profile, name, interfaces, lab_username="unh_iol", vendor="unknown", model="unknown"):
472     """
473     Used to enroll a new host of some profile
474
475     @profile: the ResourceProfile in question (by reference to a model object)
476
477     @name: the unique name of the server, currently indistinct from labid
478
479     @interfaces: interfaces should be dict from interface name (eg ens1f0) to dict of schema:
480         {
481             mac_address: <mac addr>,
482             bus_addr: <bus addr>, //this field is optional, "" is default
483         }
484
485     @lab_username: username of the lab to be added to
486
487     @vendor: vendor name of the host, such as "HPE" or "Gigabyte"
488
489     @model: specific model of the host, such as "DL380 Gen 9"
490
491     """
492     server = Server.objects.create(
493         bundle=None,
494         profile=profile,
495         config=None,
496         working=True,
497         vendor=vendor,
498         model=model,
499         labid=name,
500         lab=Lab.objects.get(lab_user__username=lab_username),
501         name=name,
502         booked=False)
503
504     for iface_prof in InterfaceProfile.objects.filter(host=profile).all():
505         mac_addr = interfaces[iface_prof.name]["mac_address"]
506         bus_addr = "unknown"
507         if "bus_addr" in interfaces[iface_prof.name].keys():
508             bus_addr = interfaces[iface_prof.name]["bus_addr"]
509
510         iface = Interface.objects.create(acts_as=None, profile=iface_prof, mac_address=mac_addr, bus_address=bus_addr)
511         iface.save()
512
513         server.interfaces.add(iface)
514         server.save()
515
516
517 def extend_booking(booking_id, days=0, hours=0, minutes=0, weeks=0):
518     """
519     Extend a booking by n <days, hours, minutes, weeks>
520
521     @booking_id: id of the booking
522
523     @days/@hours/@minutes/@weeks: the cumulative amount of delta to add to the length of the booking
524     """
525
526     booking = Booking.objects.get(id=booking_id)
527     booking.end = booking.end + timedelta(days=days, hours=hours, minutes=minutes, weeks=weeks)
528     booking.save()
529
530
531 def docs(function=None, fulltext=False):
532     """
533     Print documentation for a given function in admin_utils.
534     Call without arguments for more information
535     """
536
537     fn = None
538
539     if isinstance(function, str):
540         try:
541             fn = globals()[function]
542         except KeyError:
543             print("Couldn't find a function by the given name")
544             return
545     elif callable(function):
546         fn = function
547     else:
548         print("docs(function: callable | str, fulltext: bool) was called with a 'function' that was neither callable nor a string name of a function")
549         print("usage: docs('some_function_in_admin_utils', fulltext=True)")
550         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")
551         return
552
553     if not fn:
554         print("couldn't find a function by that name")
555
556     if not fulltext:
557         print("Pydoc documents the function as such:")
558         print(pydoc.render_doc(fn))
559     else:
560         print("The full source of the function is this:")
561         print(inspect.getsource(fn))
562
563
564 def admin_functions():
565     """
566     List functions available to call within admin_utils
567     """
568
569     return [name for name, func in inspect.getmembers(sys.modules[__name__]) if (inspect.isfunction(func) and func.__module__ == __name__)]
570
571
572 print("Hint: call `docs(<function name>)` or `admin_functions()` for help on using the admin utils")
573 print("docs(<function name>) displays documentation on a given function")
574 print("admin_functions() lists all functions available to call within this module")