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,
33 from django.contrib.auth.models import User
35 from account.models import (
40 from resource_inventory.resource_manager import ResourceManager
41 from resource_inventory.pdf_templater import PDFTemplater
43 from booking.quick_deployer import update_template
45 from datetime import timedelta
47 from django.utils import timezone
49 from booking.models import Booking
50 from notifier.manager import NotificationHandler
51 from api.models import JobFactory
53 from api.models import JobStatus
58 Utility function for printing dividers, does nothing directly useful as a utility
63 def book_host(owner_username, host_labid, lab_username, hostname, image_id, template_name, length_days=21, collaborator_usernames=[], purpose="internal", project="LaaS"):
65 creates a quick booking using the given host
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!
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
73 @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
75 @lab_username for iol is `unh_iol`, other labs will be documented here
77 @hostname the hostname that the resulting host should have set
79 @template_name the name of the (public, or user accessible) template to use for this booking
81 @length_days how long the booking should be, no hard limit currently
83 @collaborator_usernames a list of usernames for collaborators to the booking
85 @purpose what this booking will be used for
87 @project what project/group this booking is on behalf of or the owner represents
89 lab = Lab.objects.get(lab_user__username=lab_username)
90 host = Server.objects.filter(lab=lab).get(labid=host_labid)
92 print("Can't book host, already marked as booked")
98 template = ResourceTemplate.objects.filter(public=True).get(name=template_name)
99 image = Image.objects.get(id=image_id)
101 owner = User.objects.get(username=owner_username)
103 new_template = update_template(template, image, hostname, owner)
105 rmanager = ResourceManager.getInstance()
107 vlan_map = rmanager.get_vlans(new_template)
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()
113 for config in res_configs:
115 host.bundle = resource_bundle
117 rmanager.configureNetworking(resource_bundle, host, vlan_map)
122 print("Failed to book host due to error configuring it")
127 booking = Booking.objects.create(
132 start=timezone.now(),
133 end=timezone.now() + timedelta(days=int(length_days)),
134 resource=resource_bundle,
138 booking.pdf = PDFTemplater.makePDF(booking)
142 for collaborator_username in collaborator_usernames:
144 user = User.objects.get(username=collaborator_username)
145 booking.collaborators.add(user)
147 print("couldn't add user with username ", collaborator_username)
151 JobFactory.makeCompleteJob(booking)
152 NotificationHandler.notify_new_booking(booking)
155 def mark_working(host_labid, lab_username, working=True):
157 Mark a host working/not working so that it is either bookable or hidden in the dashboard.
159 @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
161 @lab_username: param of the form `unh_iol` or similar
163 @working: bool, whether by the end of execution the host should be considered working or not working
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
173 def mark_booked(host_labid, lab_username, booked=True):
175 Mark a host as booked/unbooked
177 @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
179 @lab_username: param of the form `unh_iol` or similar
181 @working: bool, whether by the end of execution the host should be considered booked or not booked
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
191 def get_host(host_labid, lab_username):
193 Returns host filtered by lab and then unique id within lab
195 @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
197 @lab_username: param of the form `unh_iol` or similar
199 lab = Lab.objects.get(lab_user__username=lab_username)
200 return Server.objects.filter(lab=lab).get(labid=host_labid)
203 def get_info(host_labid, lab_username):
205 Returns various information on the host queried by the given parameters
207 @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
209 @lab_username: param of the form `unh_iol` or similar
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)
219 info['bundle'] = binfo
222 info['config'] = cinfo
227 def map_cntt_interfaces(labid: str):
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
233 host = get_host(labid, "unh_iol")
234 host.profile = ResourceProfile.objects.get(name="HPE x86 CNTT")
236 host = get_host(labid, "unh_iol")
238 for iface in host.interfaces.all():
240 if iface.profile.name == "ens1f2":
241 new_ifprofile = InterfaceProfile.objects.get(host=host.profile, name="eno50")
243 new_ifprofile = InterfaceProfile.objects.get(host=host.profile, name=iface.profile.name)
245 iface.profile = new_ifprofile
250 def detect_leaked_hosts(labid="unh_iol"):
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
257 working_servers = Server.objects.filter(working=True, lab__lab_user__username=labid)
258 booked = working_servers.filter(booked=True)
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)
268 print("Possibly leaked:")
269 for host in filtered:
275 def booking_for_host(host_labid: str, lab_username="unh_iol"):
277 Returns the booking that this server is a part of, if any.
278 Fails with an exception if no such booking exists
280 @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object
282 @lab_username: param of the form `unh_iol` or similar
284 server = Server.objects.get(lab__lab_user__username=lab_username, lab_username=host_labid)
285 booking = server.bundle.booking_set.first()
288 print("id:", booking.id)
289 print("owner:", booking.owner)
290 print("job (id):", booking.job, "(" + str(booking.job.id) + ")")
295 def force_release_booking(booking_id: int):
297 Takes a booking id and forces the booking to end whether or not the tasks have
300 Use with caution! Hosts may or may not be released depending on other underlying issues
302 @booking_id: the id of the Booking object to be released
304 booking = Booking.objects.get(id=booking_id)
306 tasks = job.get_tasklist()
308 task.status = JobStatus.DONE
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))
318 for booking in current_booking_set:
319 for network in get_network_metadata(booking.id):
320 marked_nets.add(network["vlan_id"])
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)
327 def get_network_metadata(booking_id: int):
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}}
332 @booking_id: the id of the Booking object to be queried
334 booking = Booking.objects.get(id=booking_id)
335 bundle = booking.resource
336 pnets = PhysicalNetwork.objects.filter(bundle=bundle).all()
339 net = pnet.generic_network
340 mdata = {"vlan_id": pnet.vlan_id, "netname": net.name, "public": net.is_public}
341 metadata[net.name] = mdata
345 def print_dict_pretty(a_dict):
347 admin_utils internal function
350 print(json.dumps(a_dict, sort_keys=True, indent=4))
353 def add_profile(data):
355 Used for adding a host profile to the dashboard
357 schema (of dict passed as "data" param):
367 media_type: str ("SSD" or "HDD")
368 interface: str ("sata", "sas", "ssd", "nvme", "scsi", or "iscsi")
374 "nic_type": str ("onboard" or "pcie")
375 "order": int (compared to the other interfaces, indicates the "order" that the ports are laid out)
379 cores: int (hardware threads count)
380 architecture: str (x86_64" or "aarch64")
381 cpus: int (number of sockets)
390 base_profile = ResourceProfile.objects.create(name=data['name'], description=data['description'])
393 for lab_username in data['labs']:
394 lab = Lab.objects.get(lab_user__username=lab_username)
396 base_profile.labs.add(lab)
399 for diskname in data['disks'].keys():
400 disk = data['disks'][diskname]
402 disk_profile = DiskProfile.objects.create(name=diskname, size=disk['capacity'], media_type=disk['media_type'], interface=disk['interface'], host=base_profile)
405 for ifacename in data['interfaces'].keys():
406 iface = data['interfaces'][ifacename]
408 iface_profile = InterfaceProfile.objects.create(name=ifacename, speed=iface['speed'], nic_type=iface['nic_type'], order=iface['order'], host=base_profile)
412 cpu_prof = CpuProfile.objects.create(cores=cpu['cores'], architecture=cpu['architecture'], cpus=cpu['cpus'], cflags=cpu['cflags'], host=base_profile)
415 ram_prof = RamProfile.objects.create(amount=data['ram']['amount'], channels=data['ram']['channels'], host=base_profile)
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=""):
421 Do not call this function without reading the related source code, it may have unintended effects.
423 Used for creating a default template from some host profile
426 if not resource_profile:
427 raise Exception("No viable continuation from none resource_profile")
429 if not template_name:
430 template_name = resource_profile.name
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)
437 image_id = Image.objects.filter(host_type=resource_profile).first().id
439 image = Image.objects.get(id=image_id)
441 base = ResourceTemplate.objects.create(
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)
448 rconf = ResourceConfiguration.objects.create(profile=resource_profile, image=image, template=base, is_head_node=True, name="opnfv_host")
451 connected_interfaces = []
453 for iface_prof in InterfaceProfile.objects.filter(host=resource_profile).all():
454 iface_conf = InterfaceConfiguration.objects.create(profile=iface_prof, resource_config=rconf)
456 if iface_prof.name in connected_interface_names:
457 connected_interfaces.append(iface_conf)
459 network = Network.objects.create(name="public", bundle=base, is_public=True)
461 for iface in connected_interfaces:
462 connection = NetworkConnection.objects.create(network=network, vlan_is_tagged=interfaces_tagged)
465 iface.connections.add(connection)
466 print("adding connection to iface ", iface)
471 def add_server(profile, name, interfaces, lab_username="unh_iol", vendor="unknown", model="unknown"):
473 Used to enroll a new host of some profile
475 @profile: the ResourceProfile in question (by reference to a model object)
477 @name: the unique name of the server, currently indistinct from labid
479 @interfaces: interfaces should be dict from interface name (eg ens1f0) to dict of schema:
481 mac_address: <mac addr>,
482 bus_addr: <bus addr>, //this field is optional, "" is default
485 @lab_username: username of the lab to be added to
487 @vendor: vendor name of the host, such as "HPE" or "Gigabyte"
489 @model: specific model of the host, such as "DL380 Gen 9"
492 server = Server.objects.create(
500 lab=Lab.objects.get(lab_user__username=lab_username),
504 for iface_prof in InterfaceProfile.objects.filter(host=profile).all():
505 mac_addr = interfaces[iface_prof.name]["mac_address"]
507 if "bus_addr" in interfaces[iface_prof.name].keys():
508 bus_addr = interfaces[iface_prof.name]["bus_addr"]
510 iface = Interface.objects.create(acts_as=None, profile=iface_prof, mac_address=mac_addr, bus_address=bus_addr)
513 server.interfaces.add(iface)
517 def extend_booking(booking_id, days=0, hours=0, minutes=0, weeks=0):
519 Extend a booking by n <days, hours, minutes, weeks>
521 @booking_id: id of the booking
523 @days/@hours/@minutes/@weeks: the cumulative amount of delta to add to the length of the booking
526 booking = Booking.objects.get(id=booking_id)
527 booking.end = booking.end + timedelta(days=days, hours=hours, minutes=minutes, weeks=weeks)
531 def docs(function=None, fulltext=False):
533 Print documentation for a given function in admin_utils.
534 Call without arguments for more information
539 if isinstance(function, str):
541 fn = globals()[function]
543 print("Couldn't find a function by the given name")
545 elif callable(function):
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")
554 print("couldn't find a function by that name")
557 print("Pydoc documents the function as such:")
558 print(pydoc.render_doc(fn))
560 print("The full source of the function is this:")
561 print(inspect.getsource(fn))
564 def admin_functions():
566 List functions available to call within admin_utils
569 return [name for name, func in inspect.getmembers(sys.modules[__name__]) if (inspect.isfunction(func) and func.__module__ == __name__)]
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")