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,
34 from django.contrib.auth.models import User
36 from account.models import (
41 from resource_inventory.resource_manager import ResourceManager
42 from resource_inventory.pdf_templater import PDFTemplater
44 from booking.quick_deployer import update_template
46 from datetime import timedelta
48 from django.utils import 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 def map_cntt_interfaces(labid: str):
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
234 host = get_host(labid, "unh_iol")
235 host.profile = ResourceProfile.objects.get(name="HPE x86 CNTT")
237 host = get_host(labid, "unh_iol")
239 for iface in host.interfaces.all():
241 if iface.profile.name == "ens1f2":
242 new_ifprofile = InterfaceProfile.objects.get(host=host.profile, name="eno50")
244 new_ifprofile = InterfaceProfile.objects.get(host=host.profile, name=iface.profile.name)
246 iface.profile = new_ifprofile
251 def detect_leaked_hosts(labid="unh_iol"):
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
258 working_servers = Server.objects.filter(working=True, lab__lab_user__username=labid)
259 booked = working_servers.filter(booked=True)
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)
269 print("Possibly leaked:")
270 for host in filtered:
276 def booking_for_host(host_labid: str, lab_username="unh_iol"):
278 Returns the booking that this server is a part of, if any.
279 Fails with an exception if no such booking exists
281 @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
283 @lab_username: param of the form `unh_iol` or similar
285 server = Server.objects.get(lab__lab_user__username=lab_username, labid=host_labid)
286 booking = server.bundle.booking_set.first()
289 print("id:", booking.id)
290 print("owner:", booking.owner)
291 print("job (id):", booking.job, "(" + str(booking.job.id) + ")")
296 def force_release_booking(booking_id: int):
298 Takes a booking id and forces the booking to end whether or not the tasks have
301 Use with caution! Hosts may or may not be released depending on other underlying issues
303 @booking_id: the id of the Booking object to be released
305 booking = Booking.objects.get(id=booking_id)
307 tasks = job.get_tasklist()
309 task.status = JobStatus.DONE
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))
319 for booking in current_booking_set:
320 for network in get_network_metadata(booking.id):
321 marked_nets.add(network["vlan_id"])
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)
328 def get_network_metadata(booking_id: int):
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}}
333 @booking_id: the id of the Booking object to be queried
335 booking = Booking.objects.get(id=booking_id)
336 bundle = booking.resource
337 pnets = PhysicalNetwork.objects.filter(bundle=bundle).all()
340 net = pnet.generic_network
341 mdata = {"vlan_id": pnet.vlan_id, "netname": net.name, "public": net.is_public}
342 metadata[net.name] = mdata
346 def print_dict_pretty(a_dict):
348 admin_utils internal function
351 print(json.dumps(a_dict, sort_keys=True, indent=4))
354 def add_profile(data):
356 Used for adding a host profile to the dashboard
358 schema (of dict passed as "data" param):
368 media_type: str ("SSD" or "HDD")
369 interface: str ("sata", "sas", "ssd", "nvme", "scsi", or "iscsi")
375 "nic_type": str ("onboard" or "pcie")
376 "order": int (compared to the other interfaces, indicates the "order" that the ports are laid out)
380 cores: int (hardware threads count)
381 architecture: str (x86_64" or "aarch64")
382 cpus: int (number of sockets)
391 base_profile = ResourceProfile.objects.create(name=data['name'], description=data['description'])
394 for lab_username in data['labs']:
395 lab = Lab.objects.get(lab_user__username=lab_username)
397 base_profile.labs.add(lab)
400 for diskname in data['disks'].keys():
401 disk = data['disks'][diskname]
403 disk_profile = DiskProfile.objects.create(name=diskname, size=disk['capacity'], media_type=disk['media_type'], interface=disk['interface'], host=base_profile)
406 for ifacename in data['interfaces'].keys():
407 iface = data['interfaces'][ifacename]
409 iface_profile = InterfaceProfile.objects.create(name=ifacename, speed=iface['speed'], nic_type=iface['nic_type'], order=iface['order'], host=base_profile)
413 cpu_prof = CpuProfile.objects.create(cores=cpu['cores'], architecture=cpu['architecture'], cpus=cpu['cpus'], cflags=cpu['cflags'], host=base_profile)
416 ram_prof = RamProfile.objects.create(amount=data['ram']['amount'], channels=data['ram']['channels'], host=base_profile)
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=""):
422 Do not call this function without reading the related source code, it may have unintended effects.
424 Used for creating a default template from some host profile
427 if not resource_profile:
428 raise Exception("No viable continuation from none resource_profile")
430 if not template_name:
431 template_name = resource_profile.name
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)
438 image_id = Image.objects.filter(host_type=resource_profile).first().id
440 image = Image.objects.get(id=image_id)
442 base = ResourceTemplate.objects.create(
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)
449 rconf = ResourceConfiguration.objects.create(profile=resource_profile, image=image, template=base, is_head_node=True, name="opnfv_host")
452 connected_interfaces = []
454 for iface_prof in InterfaceProfile.objects.filter(host=resource_profile).all():
455 iface_conf = InterfaceConfiguration.objects.create(profile=iface_prof, resource_config=rconf)
457 if iface_prof.name in connected_interface_names:
458 connected_interfaces.append(iface_conf)
460 network = Network.objects.create(name="public", bundle=base, is_public=True)
462 for iface in connected_interfaces:
463 connection = NetworkConnection.objects.create(network=network, vlan_is_tagged=interfaces_tagged)
466 iface.connections.add(connection)
467 print("adding connection to iface ", iface)
472 def add_server(profile, name, interfaces, lab_username="unh_iol", vendor="unknown", model="unknown"):
474 Used to enroll a new host of some profile
476 @profile: the ResourceProfile in question (by reference to a model object)
478 @name: the unique name of the server, currently indistinct from labid
480 @interfaces: interfaces should be dict from interface name (eg ens1f0) to dict of schema:
482 mac_address: <mac addr>,
483 bus_addr: <bus addr>, //this field is optional, "" is default
486 @lab_username: username of the lab to be added to
488 @vendor: vendor name of the host, such as "HPE" or "Gigabyte"
490 @model: specific model of the host, such as "DL380 Gen 9"
493 server = Server.objects.create(
501 lab=Lab.objects.get(lab_user__username=lab_username),
505 for iface_prof in InterfaceProfile.objects.filter(host=profile).all():
506 mac_addr = interfaces[iface_prof.name]["mac_address"]
508 if "bus_addr" in interfaces[iface_prof.name].keys():
509 bus_addr = interfaces[iface_prof.name]["bus_addr"]
511 iface = Interface.objects.create(acts_as=None, profile=iface_prof, mac_address=mac_addr, bus_address=bus_addr)
514 server.interfaces.add(iface)
518 def extend_booking(booking_id, days=0, hours=0, minutes=0, weeks=0):
520 Extend a booking by n <days, hours, minutes, weeks>
522 @booking_id: id of the booking
524 @days/@hours/@minutes/@weeks: the cumulative amount of delta to add to the length of the booking
527 booking = Booking.objects.get(id=booking_id)
528 booking.end = booking.end + timedelta(days=days, hours=hours, minutes=minutes, weeks=weeks)
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!
537 cif = GeneratedCloudConfig.objects.create(resource_id=res.labid, booking=b, rconfig=res.config)
539 cif = CloudInitFile.create(priority=0, text=cif.serialize())
541 res.config.cloud_init_files.add(cif)
545 def set_job_new(job_id):
546 j = Job.objects.get(id=job_id)
548 regenerate_cloud_configs(b.id)
549 for task in j.get_tasklist():
550 task.status = JobStatus.NEW
552 j.status = JobStatus.NEW
556 def docs(function=None, fulltext=False):
558 Print documentation for a given function in admin_utils.
559 Call without arguments for more information
564 if isinstance(function, str):
566 fn = globals()[function]
568 print("Couldn't find a function by the given name")
570 elif callable(function):
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")
579 print("couldn't find a function by that name")
582 print("Pydoc documents the function as such:")
583 print(pydoc.render_doc(fn))
585 print("The full source of the function is this:")
586 print(inspect.getsource(fn))
589 def admin_functions():
591 List functions available to call within admin_utils
594 return [name for name, func in inspect.getmembers(sys.modules[__name__]) if (inspect.isfunction(func) and func.__module__ == __name__)]
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")