1 ##############################################################################
2 # Copyright (c) 2021 Sawyer Bergeron and others.
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 ##############################################################################
10 from resource_inventory.models import (
18 ResourceConfiguration,
20 InterfaceConfiguration,
36 from django.contrib.auth.models import User
38 from account.models import (
43 from resource_inventory.resource_manager import ResourceManager
44 from resource_inventory.pdf_templater import PDFTemplater
46 from booking.quick_deployer import update_template
48 from datetime import timedelta, date, datetime, timezone
50 from booking.models import Booking
51 from notifier.manager import NotificationHandler
52 from api.models import JobFactory
54 from api.models import JobStatus, Job, GeneratedCloudConfig
59 Utility function for printing dividers, does nothing directly useful as a utility
64 def book_host(owner_username, host_labid, lab_username, hostname, image_id, template_name, length_days=21, collaborator_usernames=[], purpose="internal", project="LaaS"):
66 creates a quick booking using the given host
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!
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
74 @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
76 @lab_username for iol is `unh_iol`, other labs will be documented here
78 @hostname the hostname that the resulting host should have set
80 @template_name the name of the (public, or user accessible) template to use for this booking
82 @length_days how long the booking should be, no hard limit currently
84 @collaborator_usernames a list of usernames for collaborators to the booking
86 @purpose what this booking will be used for
88 @project what project/group this booking is on behalf of or the owner represents
90 lab = Lab.objects.get(lab_user__username=lab_username)
91 host = Server.objects.filter(lab=lab).get(labid=host_labid)
93 print("Can't book host, already marked as booked")
99 template = ResourceTemplate.objects.filter(public=True).get(name=template_name)
100 image = Image.objects.get(id=image_id)
102 owner = User.objects.get(username=owner_username)
104 new_template = update_template(template, image, hostname, owner)
106 rmanager = ResourceManager.getInstance()
108 vlan_map = rmanager.get_vlans(new_template)
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()
114 for config in res_configs:
116 host.bundle = resource_bundle
118 rmanager.configureNetworking(resource_bundle, host, vlan_map)
123 print("Failed to book host due to error configuring it")
128 booking = Booking.objects.create(
133 start=timezone.now(),
134 end=timezone.now() + timedelta(days=int(length_days)),
135 resource=resource_bundle,
139 booking.pdf = PDFTemplater.makePDF(booking)
143 for collaborator_username in collaborator_usernames:
145 user = User.objects.get(username=collaborator_username)
146 booking.collaborators.add(user)
148 print("couldn't add user with username ", collaborator_username)
152 JobFactory.makeCompleteJob(booking)
153 NotificationHandler.notify_new_booking(booking)
156 def mark_working(host_labid, lab_username, working=True):
158 Mark a host working/not working so that it is either bookable or hidden in the dashboard.
160 @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
162 @lab_username: param of the form `unh_iol` or similar
164 @working: bool, whether by the end of execution the host should be considered working or not working
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
174 def mark_booked(host_labid, lab_username, booked=True):
176 Mark a host as booked/unbooked
178 @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
180 @lab_username: param of the form `unh_iol` or similar
182 @working: bool, whether by the end of execution the host should be considered booked or not booked
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
192 def get_host(host_labid, lab_username):
194 Returns host filtered by lab and then unique id within lab
196 @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
198 @lab_username: param of the form `unh_iol` or similar
200 lab = Lab.objects.get(lab_user__username=lab_username)
201 return Server.objects.filter(lab=lab).get(labid=host_labid)
204 def get_info(host_labid, lab_username):
206 Returns various information on the host queried by the given parameters
208 @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
210 @lab_username: param of the form `unh_iol` or similar
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)
220 info['bundle'] = binfo
223 info['config'] = cinfo
228 class CumulativeData:
233 def __init__(self, file_writer):
234 self.file_writer = file_writer
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
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])
248 def get_years_booking_data(start_year=None, end_year=None):
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.
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)
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)
263 if (start.year == end.year - 1):
264 file_name = "yearly_booking_data_" + str(start.year) + ".csv"
266 file_name = "yearly_booking_data_" + str(start.year) + "-" + str(end.year - 1) + ".csv"
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(
285 for booking in Booking.objects.filter(start__gte=start, start__lte=end):
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"
291 for b in booking_filter:
295 for u in user_filter:
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
302 for c in booking.collaborators.all():
303 collaborators.append(c.username)
306 cumulative_data.account(booking, usage_days)
307 file_writer.writerow([
309 str(booking.project),
310 str(booking.purpose),
311 str(booking.owner.username),
312 ','.join(collaborators),
313 str(booking.ext_count),
318 cumulative_data.write_cumulative()
321 def map_cntt_interfaces(labid: str):
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
327 host = get_host(labid, "unh_iol")
328 host.profile = ResourceProfile.objects.get(name="HPE x86 CNTT")
330 host = get_host(labid, "unh_iol")
332 for iface in host.interfaces.all():
334 if iface.profile.name == "ens1f2":
335 new_ifprofile = InterfaceProfile.objects.get(host=host.profile, name="eno50")
337 new_ifprofile = InterfaceProfile.objects.get(host=host.profile, name=iface.profile.name)
339 iface.profile = new_ifprofile
344 def detect_leaked_hosts(labid="unh_iol"):
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
351 working_servers = Server.objects.filter(working=True, lab__lab_user__username=labid)
352 booked = working_servers.filter(booked=True)
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)
362 print("Possibly leaked:")
363 for host in filtered:
369 def booking_for_host(host_labid: str, lab_username="unh_iol"):
371 Returns the booking that this server is a part of, if any.
372 Fails with an exception if no such booking exists
374 @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
376 @lab_username: param of the form `unh_iol` or similar
378 server = Server.objects.get(lab__lab_user__username=lab_username, labid=host_labid)
379 booking = server.bundle.booking_set.first()
382 print("id:", booking.id)
383 print("owner:", booking.owner)
384 print("job (id):", booking.job, "(" + str(booking.job.id) + ")")
389 def force_release_booking(booking_id: int):
391 Takes a booking id and forces the booking to end whether or not the tasks have
394 Use with caution! Hosts may or may not be released depending on other underlying issues
396 @booking_id: the id of the Booking object to be released
398 booking = Booking.objects.get(id=booking_id)
400 tasks = job.get_tasklist()
402 task.status = JobStatus.DONE
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))
412 for booking in current_booking_set:
413 for network in get_network_metadata(booking.id):
414 marked_nets.add(network["vlan_id"])
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)
421 def get_network_metadata(booking_id: int):
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}}
426 @booking_id: the id of the Booking object to be queried
428 booking = Booking.objects.get(id=booking_id)
429 bundle = booking.resource
430 pnets = PhysicalNetwork.objects.filter(bundle=bundle).all()
433 net = pnet.generic_network
434 mdata = {"vlan_id": pnet.vlan_id, "netname": net.name, "public": net.is_public}
435 metadata[net.name] = mdata
439 def print_dict_pretty(a_dict):
441 admin_utils internal function
444 print(json.dumps(a_dict, sort_keys=True, indent=4))
447 def import_host(filenames):
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
455 for filename in filenames:
458 file = open("dashboard/" + filename + "-import.yaml", "r")
459 data = yaml.safe_load(file)
461 # if a new profile is needed create one and a matching template
462 if (data["new_profile"]):
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,
479 print(" Template: " + data["temp_name"] + " created!")
483 ResourceProfile.objects.get(name=data["name"]),
491 print(data["hostname"] + " imported!")
494 def convert_inspect_results(files):
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: []
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)
505 # gather data about disks
507 for i in data["disk"]:
509 # don't include loops in disks
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\">",
517 # gather 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"],
530 "cores": data["cpu"]["cores"],
531 "architecture": data["cpu"]["arch"],
532 "cpus": data["cpu"]["cpus"],
533 "cflags": "<cflags string>",
538 "amount": data["memory"][:-1],
539 "channels": "<int of ram channels used>",
542 # assemble data for host import file
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\")",
555 "interfaces": interface_data,
558 "vendor": "<host vendor>",
559 "model": "<host model>",
562 # export data as yaml
563 yaml.dump(import_data, output)
566 def add_profile(data):
568 Used for adding a host profile to the dashboard
570 schema (of dict passed as "data" param):
580 media_type: str ("SSD" or "HDD")
581 interface: str ("sata", "sas", "ssd", "nvme", "scsi", or "iscsi")
587 "nic_type": str ("onboard" or "pcie")
588 "order": int (compared to the other interfaces, indicates the "order" that the ports are laid out)
592 cores: int (hardware threads count)
593 architecture: str (x86_64" or "aarch64")
594 cpus: int (number of sockets)
603 base_profile = ResourceProfile.objects.create(name=data['name'], description=data['description'])
606 for lab_username in data['labs']:
607 lab = Lab.objects.get(lab_user__username=lab_username)
609 base_profile.labs.add(lab)
612 for diskname in data['disks'].keys():
613 disk = data['disks'][diskname]
615 disk_profile = DiskProfile.objects.create(name=diskname, size=disk['capacity'], media_type=disk['media_type'], interface=disk['interface'], host=base_profile)
618 for ifacename in data['interfaces'].keys():
619 iface = data['interfaces'][ifacename]
621 iface_profile = InterfaceProfile.objects.create(name=ifacename, speed=iface['speed'], nic_type=iface['nic_type'], order=iface['order'], host=base_profile)
625 cpu_prof = CpuProfile.objects.create(cores=cpu['cores'], architecture=cpu['architecture'], cpus=cpu['cpus'], cflags=cpu['cflags'], host=base_profile)
628 ram_prof = RamProfile.objects.create(amount=data['ram']['amount'], channels=data['ram']['channels'], host=base_profile)
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=""):
634 Do not call this function without reading the related source code, it may have unintended effects.
636 Used for creating a default template from some host profile
639 if not resource_profile:
640 raise Exception("No viable continuation from none resource_profile")
642 if not template_name:
643 template_name = resource_profile.name
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)
650 image_id = Image.objects.filter(host_type=resource_profile).first().id
652 image = Image.objects.get(id=image_id)
654 base = ResourceTemplate.objects.create(
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)
661 rconf = ResourceConfiguration.objects.create(profile=resource_profile, image=image, template=base, is_head_node=True, name="opnfv_host")
664 connected_interfaces = []
666 for iface_prof in InterfaceProfile.objects.filter(host=resource_profile).all():
667 iface_conf = InterfaceConfiguration.objects.create(profile=iface_prof, resource_config=rconf)
669 if iface_prof.name in connected_interface_names:
670 connected_interfaces.append(iface_conf)
672 network = Network.objects.create(name="public", bundle=base, is_public=True)
674 for iface in connected_interfaces:
675 connection = NetworkConnection.objects.create(network=network, vlan_is_tagged=interfaces_tagged)
678 iface.connections.add(connection)
679 print("adding connection to iface ", iface)
684 def add_server(profile, name, interfaces, lab_username="unh_iol", vendor="unknown", model="unknown"):
686 Used to enroll a new host of some profile
688 @profile: the ResourceProfile in question (by reference to a model object)
690 @name: the unique name of the server, currently indistinct from labid
692 @interfaces: interfaces should be dict from interface name (eg ens1f0) to dict of schema:
694 mac_address: <mac addr>,
695 bus_addr: <bus addr>, //this field is optional, "" is default
698 @lab_username: username of the lab to be added to
700 @vendor: vendor name of the host, such as "HPE" or "Gigabyte"
702 @model: specific model of the host, such as "DL380 Gen 9"
705 server = Server.objects.create(
713 lab=Lab.objects.get(lab_user__username=lab_username),
717 for iface_prof in InterfaceProfile.objects.filter(host=profile).all():
718 mac_addr = interfaces[iface_prof.name]["mac_address"]
720 if "bus_addr" in interfaces[iface_prof.name].keys():
721 bus_addr = interfaces[iface_prof.name]["bus_addr"]
723 iface = Interface.objects.create(acts_as=None, profile=iface_prof, mac_address=mac_addr, bus_address=bus_addr)
726 server.interfaces.add(iface)
730 def extend_booking(booking_id, days=0, hours=0, minutes=0, weeks=0):
732 Extend a booking by n <days, hours, minutes, weeks>
734 @booking_id: id of the booking
736 @days/@hours/@minutes/@weeks: the cumulative amount of delta to add to the length of the booking
739 booking = Booking.objects.get(id=booking_id)
740 booking.end = booking.end + timedelta(days=days, hours=hours, minutes=minutes, weeks=weeks)
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!
749 cif = GeneratedCloudConfig.objects.create(resource_id=res.labid, booking=b, rconfig=res.config)
751 cif = CloudInitFile.create(priority=0, text=cif.serialize())
753 res.config.cloud_init_files.add(cif)
757 def set_job_new(job_id):
758 j = Job.objects.get(id=job_id)
760 regenerate_cloud_configs(b.id)
761 for task in j.get_tasklist():
762 task.status = JobStatus.NEW
764 j.status = JobStatus.NEW
768 def docs(function=None, fulltext=False):
770 Print documentation for a given function in admin_utils.
771 Call without arguments for more information
776 if isinstance(function, str):
778 fn = globals()[function]
780 print("Couldn't find a function by the given name")
782 elif callable(function):
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")
791 print("couldn't find a function by that name")
794 print("Pydoc documents the function as such:")
795 print(pydoc.render_doc(fn))
797 print("The full source of the function is this:")
798 print(inspect.getsource(fn))
801 def admin_functions():
803 List functions available to call within admin_utils
806 return [name for name, func in inspect.getmembers(sys.modules[__name__]) if (inspect.isfunction(func) and func.__module__ == __name__)]
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")