import json
import logging
import numbers
import re
import time
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, List, Optional, Union
from blazarclient.exception import BlazarClientException
from IPython.display import display
from ipywidgets import HTML
from packaging.version import Version
from chi import context, util
from .clients import blazar
from .context import _is_ipynb
from .exception import CHIValueError, ResourceError, ServiceError
from .hardware import Node
from .network import PUBLIC_NETWORK, get_network_id, list_floating_ips
from .util import utcnow
if TYPE_CHECKING:
from typing import Pattern
LOG = logging.getLogger(__name__)
class ErrorParsers:
NOT_ENOUGH_RESOURCES: "Pattern" = re.compile(
r"not enough (?P<resource_type>([\w\s\-\._]+)) available"
)
BLAZAR_TIME_FORMAT = "%Y-%m-%d %H:%M"
DEFAULT_NODE_TYPE = "compute_skylake"
DEFAULT_LEASE_LENGTH = timedelta(days=1)
DEFAULT_NETWORK_RESOURCE_PROPERTIES = ["==", "$physical_network", "physnet1"]
[docs]
def lease_create_args(
neutronclient,
name=None,
start="now",
end=None,
length=None,
nodes=1,
node_resource_properties=None,
fips=0,
networks=0,
network_resource_properties=DEFAULT_NETWORK_RESOURCE_PROPERTIES,
):
"""
.. deprecated:: 1.0
Generates the nested object that needs to be sent to the Blazar client
to create the lease. Provides useful defaults for Chameleon.
:param str name: name of lease. If ``None``, generates a random name.
:param str/datetime start: when to start lease as a
:py:class:`datetime.datetime` object, or if the string ``'now'``,
starts in about a minute.
:param length: length of time as a :py:class:`datetime.timedelta` object or
number of seconds as a number. Defaults to 1 day.
:param datetime.datetime end: when to end the lease. Provide only this or
`length`, not both.
:param int nodes: number of nodes to reserve.
:param resource_properties: object that is JSON-encoded and sent as the
``resource_properties`` value to Blazar. Commonly used to specify
node types.
"""
if start == "now":
start = utcnow() + timedelta(seconds=70)
if length is None and end is None:
length = DEFAULT_LEASE_LENGTH
elif length is not None and end is not None:
raise CHIValueError("provide either 'length' or 'end', not both")
if end is None:
if isinstance(length, numbers.Number):
length = timedelta(seconds=length)
end = start + length
reservations = []
if nodes > 0:
if node_resource_properties:
node_resource_properties = json.dumps(node_resource_properties)
reservations += [
{
"resource_type": "physical:host",
"resource_properties": node_resource_properties or "",
"hypervisor_properties": "",
"min": str(nodes),
"max": str(nodes),
}
]
if fips > 0:
reservations += [
{
"resource_type": "virtual:floatingip",
"network_id": get_network_id(PUBLIC_NETWORK),
"amount": fips,
}
]
if networks > 0:
if network_resource_properties:
network_resource_properties = json.dumps(network_resource_properties)
reservations += [
{
"resource_type": "network",
"resource_properties": network_resource_properties or "",
"network_name": f"{name}-net{idx}",
}
for idx in range(networks)
]
return {
"name": name,
"start": start.strftime(BLAZAR_TIME_FORMAT),
"end": end.strftime(BLAZAR_TIME_FORMAT),
"reservations": reservations,
"events": [],
}
[docs]
def lease_create_nodetype(*args, **kwargs):
"""
.. deprecated:: 1.0
Wrapper for :py:func:`lease_create_args` that adds the
``resource_properties`` payload to specify node type.
:param str node_type: Node type to filter by, ``compute_skylake``, et al.
:raises ValueError: if there is no `node_type` named argument.
"""
try:
node_type = kwargs.pop("node_type")
except KeyError:
raise CHIValueError("no node_type specified")
kwargs["node_resource_properties"] = ["==", "$node_type", node_type]
return lease_create_args(*args, **kwargs)
[docs]
class Lease:
"""
Represents a lease in the CHI system.
Args:
name (str): The name of the lease.
start_date (datetime, optional): The start date of the lease. Defaults to None.
end_date (datetime, optional): The end date of the lease. Defaults to None.
duration (timedelta, optional): The duration of the lease. Defaults to None.
lease_json (dict, optional): JSON representation of the lease. Defaults to None.
Attributes:
name (str): The name of the lease.
start_date (str): The start date of the lease in the format specified by BLAZAR_TIME_FORMAT.
end_date (str): The end date of the lease in the format specified by BLAZAR_TIME_FORMAT.
id (str): The ID of the lease.
status (str): The status of the lease.
user_id (str): The ID of the user associated with the lease.
project_id (str): The ID of the project associated with the lease.
created_at (datetime): The creation date of the lease.
device_reservations (list): List of device reservations associated with the lease.
node_reservations (list): List of node reservations associated with the lease.
fip_reservations (list): List of floating IP reservations associated with the lease.
network_reservations (list): List of network reservations associated with the lease.
events (list): List of events associated with the lease.
"""
def __init__(
self,
name: Optional[str] = None,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
duration: Optional[timedelta] = None,
lease_json: Optional[dict] = None,
):
self.id = None
self.status = None
self.user_id = None
self.project_id = None
self.created_at = None
self.device_reservations = []
self.node_reservations = []
self.fip_reservations = []
self.network_reservations = []
self._events = []
if lease_json:
self._populate_from_json(lease_json)
else:
if name is None:
raise CHIValueError(
"Name must be specified when lease_json is not provided"
)
self.name = name
if start_date:
self.start_date = start_date.strftime(BLAZAR_TIME_FORMAT)
else:
self.start_date = "now"
if end_date and duration:
raise CHIValueError("Specify either end_date or duration, not both")
elif end_date:
self.end_date = end_date.strftime(BLAZAR_TIME_FORMAT)
elif duration:
self.end_date = (utcnow() + duration).strftime(BLAZAR_TIME_FORMAT)
else:
raise CHIValueError("Either end_date or duration must be specified")
def _populate_from_json(self, lease_json):
self.name = lease_json.get("name")
self.id = lease_json.get("id")
self.status = lease_json.get("status")
self.user_id = lease_json.get("user_id")
self.project_id = lease_json.get("project_id")
self.created_at = datetime.fromisoformat(lease_json.get("created_at"))
self.start_date = datetime.strptime(
lease_json.get("start_date"), "%Y-%m-%dT%H:%M:%S.%f"
)
self.end_date = datetime.strptime(
lease_json.get("end_date"), "%Y-%m-%dT%H:%M:%S.%f"
)
self.created_at = datetime.strptime(
lease_json.get("created_at"), "%Y-%m-%d %H:%M:%S"
)
self.device_reservations.clear()
self.node_reservations.clear()
self.fip_reservations.clear()
self.network_reservations.clear()
for reservation in lease_json.get("reservations", []):
resource_type = reservation.get("resource_type")
if resource_type == "device":
self.device_reservations.append(reservation)
if resource_type == "physical:host":
self.node_reservations.append(reservation)
elif resource_type == "virtual:floatingip":
self.fip_reservations.append(reservation)
elif resource_type == "network":
self.network_reservations.append(reservation)
# self.events = lease_json.get('events', [])
[docs]
def add_device_reservation(
self,
amount: int = None,
machine_type: str = None,
device_model: str = None,
device_name: str = None,
):
"""
Add a IoT device reservation to the list of device reservations.
Args:
amount (int, optional): The number of devices to reserve. Defaults to None.
machine_type (str, optional): The type of machine to reserve. Defaults to None.
device_model (str, optional): The model of the device to reserve. Defaults to None.
device_name (str, optional): The name of the device to reserve. Defaults to None.
"""
add_device_reservation(
reservation_list=self.device_reservations,
count=amount,
machine_name=machine_type,
device_model=device_model,
device_name=device_name,
)
[docs]
def add_node_reservation(
self,
amount: int = None,
node_type: str = None,
node_name: str = None,
nodes: List[Node] = None,
):
"""
Add a node reservation to the lease.
Parameters:
- amount (int): The number of nodes to reserve.
- node_type (str): The type of nodes to reserve.
- node_name (str): The name of the node to reserve.
- nodes (List[Node]): A list of Node objects to reserve.
Raises:
- CHIValueError: If nodes are specified, no other arguments should be included.
"""
if nodes:
if any([amount, node_type, node_name]):
raise CHIValueError(
"When specifying nodes, no other arguments should be included"
)
for node in nodes:
add_node_reservation(
reservation_list=self.node_reservations, node_name=node.name
)
else:
add_node_reservation(
reservation_list=self.node_reservations,
count=amount,
node_type=node_type,
node_name=node_name,
)
[docs]
def add_fip_reservation(self, amount: int):
"""
Add a reservation for a floating IP address to the list of FIP reservations.
Args:
amount (int): The number of reservations to add.
Returns:
None
"""
add_fip_reservation(reservation_list=self.fip_reservations, count=amount)
[docs]
def add_network_reservation(
self, network_name: str, usage_type: str = None, stitch_provider: str = None
):
"""
Add a network reservation to the list of network reservations.
Args:
network_name (str): The name of the network to be reserved.
usage_type (str, optional): The type of usage for the network reservation. Defaults to None.
stitch_provider (str, optional): The stitch provider for the network reservation. Defaults to None.
"""
add_network_reservation(
reservation_list=self.network_reservations,
network_name=network_name,
usage_type=usage_type,
stitch_provider=stitch_provider,
)
[docs]
def submit(
self,
wait_for_active: bool = True,
wait_timeout: int = 300,
show: Optional[str] = None,
idempotent: bool = False,
):
"""
Submits the lease for creation.
Args:
wait_for_active (bool, optional): Whether to wait for the lease to become active. Defaults to True.
wait_timeout (int, optional): The maximum time to wait for the lease to become active, in seconds. Defaults to 300.
show (Optional[str], optional): The types of lease information to display. Defaults to None, options are "widget", "text".
idempotent (bool, optional): Whether to create the lease only if it doesn't already exist. Defaults to False.
Raises:
ResourceError: If unable to create the lease.
Returns:
None
"""
if idempotent:
existing_lease = _get_lease_from_blazar(self.name)
if existing_lease and existing_lease["status"] != "TERMINATED":
print("Found existing lease")
self._populate_from_json(existing_lease)
if wait_for_active:
self.wait(status="active", timeout=wait_timeout)
if show:
self.show(type=show, wait_for_active=wait_for_active)
return
reservations = (
self.device_reservations
+ self.node_reservations
+ self.fip_reservations
+ self.network_reservations
)
response = create_lease(
lease_name=self.name,
reservations=reservations,
start_date=self.start_date,
end_date=self.end_date,
)
if response:
self._populate_from_json(response)
else:
raise ResourceError("Unable to make lease")
if wait_for_active:
self.wait(status="active", timeout=wait_timeout)
if show:
self.show(type=show, wait_for_active=wait_for_active)
[docs]
def wait(self, status="active", show: str = "widget", timeout: int = 500):
"""
Waits for the lease's status to reach the specified status.
Args:
status (str): The status to wait for. Defaults to "ACTIVE".
show (str, optional): The type of server information to display after creation. Defaults to "widget".
timeout (int): How long to wait for lease to start
Raises:
ServiceError: If the server does not reach the specified status within the timeout period.
Returns:
None
"""
print("Waiting for lease to start... This can take up to 60 seconds")
pb = util.TimerProgressBar()
if show == "widget" and _is_ipynb():
pb.display()
def _callback():
self.refresh()
if self.status == status.upper() or self.status == "ERROR":
print(f"Lease {self.name} has reached status {self.status.lower()}")
return True
return False
res = pb.wait(_callback, 60, timeout)
if not res:
raise ServiceError(
f"Lease did not reach '{status}' status within 120 seconds, check its start time."
)
def refresh(self):
if self.id:
lease_data = blazar().lease.get(self.id)
self._populate_from_json(lease_data)
else:
raise ResourceError(
"Lease object does not yet have a valid id, please submit the object for creation first"
)
def delete(self):
if self.id:
blazar().lease.delete(self.id)
self.id = None
self.status = "DELETED"
else:
raise ResourceError(
"Lease object does not yet have a valid id, please submit the object for creation first"
)
def show(self, type=["text", "widget"], wait_for_active=False):
if wait_for_active:
self.wait(status="active")
if "widget" in type and _is_ipynb():
self._show_widget()
if "text" in type:
self._show_text()
def _show_widget(self):
html_content = f"""
<h2>Lease Details</h2>
<table>
<tr><th>Name</th><td>{self.name}</td></tr>
<tr><th>ID</th><td>{self.id or 'N/A'}</td></tr>
<tr><th>Status</th><td>{self.status or 'N/A'}</td></tr>
<tr><th>Start Date</th><td>{self.start_date or 'N/A'}</td></tr>
<tr><th>End Date</th><td>{self.end_date or 'N/A'}</td></tr>
<tr><th>User ID</th><td>{self.user_id or 'N/A'}</td></tr>
<tr><th>Project ID</th><td>{self.project_id or 'N/A'}</td></tr>
</table>
<h3>Device Reservations</h3>
<ul>
{"".join(f"<li>ID: {r.get('id', 'N/A')}, Status: {r.get('status', 'N/A')}, Resource type: {r.get('resource_type', 'N/A')}, Min: {r.get('min', 'N/A')}, Max: {r.get('max', 'N/A')}</li>" for r in self.device_reservations)}
</ul>
<h3>Node Reservations</h3>
<ul>
{"".join(f"<li>ID: {r.get('id', 'N/A')}, Status: {r.get('status', 'N/A')}, Resource type: {r.get('resource_type', 'N/A')}, Min: {r.get('min', 'N/A')}, Max: {r.get('max', 'N/A')}</li>" for r in self.node_reservations)}
</ul>
<h3>Floating IP Reservations</h3>
<ul>
{"".join(f"<li>ID: {r.get('id', 'N/A')}, Status: {r.get('status', 'N/A')}, Resource type: {r.get('resource_type', 'N/A')}, Amount: {r.get('amount', 'N/A')}</li>" for r in self.fip_reservations)}
</ul>
<h3>Network Reservations</h3>
<ul>
{"".join(f"<li>ID: {r.get('id', 'N/A')}, Status: {r.get('status', 'N/A')}, Resource type: {r.get('resource_type', 'N/A')}, Network Name: {r.get('network_name', 'N/A')}</li>" for r in self.network_reservations)}
</ul>
<h3>Events</h3>
<ul>
{"".join(f"<li>Type: {e.get('event_type', 'N/A')}, Time: {e.get('time', 'N/A')}, Status: {e.get('status', 'N/A')}</li>" for e in self.events)}
</ul>
"""
widget = HTML(html_content)
display(widget)
def _show_text(self):
print("Lease Details:")
print(f"Name: {self.name}")
print(f"ID: {self.id or 'N/A'}")
print(f"Status: {self.status or 'N/A'}")
print(f"Start Date: {self.start_date or 'N/A'}")
print(f"End Date: {self.end_date or 'N/A'}")
print(f"User ID: {self.user_id or 'N/A'}")
print(f"Project ID: {self.project_id or 'N/A'}")
print("\nNode Reservations:")
for r in self.node_reservations:
print(
f"ID: {r.get('id', 'N/A')}, Status: {r.get('status', 'N/A')}, Min: {r.get('min', 'N/A')}, Max: {r.get('max', 'N/A')}"
)
print("\nFloating IP Reservations:")
for r in self.fip_reservations:
print(
f"ID: {r.get('id', 'N/A')}, Status: {r.get('status', 'N/A')}, Amount: {r.get('amount', 'N/A')}"
)
print("\nNetwork Reservations:")
for r in self.network_reservations:
print(
f"ID: {r.get('id', 'N/A')}, Status: {r.get('status', 'N/A')}, Network Name: {r.get('network_name', 'N/A')}"
)
print("\nEvents:")
for e in self.events:
print(
f"Type: {e.get('event_type', 'N/A')}, Time: {e.get('time', 'N/A')}, Status: {e.get('status', 'N/A')}"
)
@property
def events(self):
if self.id:
# TODO Fetch latest events from Blazar API
pass
return self._events
@property
def status(self):
if self.id:
self.refresh()
return self._status
@status.setter
def status(self, value):
self._status = value
[docs]
def get_reserved_floating_ips(self):
"""Get reserved floating ips from this lease
Returns:
List[str] of fip addresses
"""
fips = list_floating_ips()
return [
fip["floating_ip_address"]
for fip in fips
if any(
f"reservation:{r['id']}" in fip["tags"] for r in self.fip_reservations
)
]
def _format_resource_properties(user_constraints, extra_constraints):
if user_constraints:
if user_constraints[0] == "and":
# Already a compound constraint
resource_properties = user_constraints + extra_constraints
else:
resource_properties = ["and", user_constraints] + extra_constraints
else:
if len(extra_constraints) < 2:
# Possibly a compount constraint if multiple kwarg helpers used
resource_properties = extra_constraints[0] if extra_constraints else []
else:
resource_properties = ["and"] + extra_constraints
return resource_properties
[docs]
def add_node_reservation(
reservation_list,
count=1,
resource_properties=None,
node_type=None,
node_name=None,
architecture=None,
):
"""
.. deprecated:: 1.0
Add a node reservation to a reservation list.
Args:
reservation_list (list[dict]): The list of reservations to add to.
The list will be extended in-place.
count (int): The number of nodes of the given type to request.
(Default 1).
resource_properties (list): A list of resource property constraints. These take
the form [<operation>, <search_key>, <search_value>], e.g.::
["==", "$node_type", "some-node-type"]: filter the reservation to only
nodes with a `node_type` matching "some-node-type".
[">", "$architecture.smt_size", 40]: filter to nodes having more than 40
(hyperthread) cores.
node_name (str): The specific node name to request. If None, the reservation will
target any node of the node_type.
node_type (str): The node type to request. If None, the reservation will not
target any particular node type. If `resource_properties` is defined, the
node type constraint is added to the existing property constraints.
architecture (str): The node architecture to request. If `resource_properties`
is defined, the architecture constraint is added to the existing property
constraints.
"""
user_constraints = (resource_properties or []).copy()
extra_constraints = []
if node_type:
extra_constraints.append(["==", "$node_type", node_type])
if architecture:
extra_constraints.append(["==", "$architecture.platform_type", architecture])
if node_name:
if (
count != 1
or node_type is not None
or resource_properties is not None
or architecture is not None
):
raise CHIValueError(
"If node name is specified, no other resource constraint can be specified"
)
extra_constraints.append(["==", "$node_name", node_name])
resource_properties = _format_resource_properties(
user_constraints, extra_constraints
)
reservation_list.append(
{
"resource_type": "physical:host",
"resource_properties": json.dumps(resource_properties),
"hypervisor_properties": "",
"min": count,
"max": count,
}
)
[docs]
def get_node_reservation(
lease_ref, count=None, resource_properties=None, node_type=None, architecture=None
):
"""
.. deprecated:: 1.0
Retrieve a reservation ID for a node reservation.
The reservation ID is useful to have when launching bare metal instances.
Args:
lease_ref (str): The ID or name of the lease.
count (int): An optional count of nodes the desired reservation was
made for. Use this if you have multiple reservations under a lease.
resource_properties (list): An optional set of resource property constraints
the desired reservation was made under. Use this if you have multiple
reservations under a lease.
node_type (str): An optional node type the desired reservation was
made for. Use this if you have multiple reservations under a lease.
architecture (str): An optional node architecture the desired reservation was
made for. Use this if you have multiple reservations under a lease.
Returns:
The ID of the reservation, if found.
Raises:
ValueError: If no reservation was found, or multiple were found.
"""
def _find_node_reservation(res):
if res.get("resource_type") != "physical:host":
return False
if count is not None and not all(
int(res.get(key, -1)) == count for key in ["min", "max"]
):
return False
rp = res.get("resource_properties")
if node_type is not None and node_type not in rp:
return False
if architecture is not None and architecture not in rp:
return False
if resource_properties is not None and json.dumps(rp) != resource_properties:
return False
return True
res = _reservation_matching(lease_ref, _find_node_reservation)
return res["id"]
[docs]
def get_device_reservation(
lease_ref, count=None, machine_name=None, device_model=None, device_name=None
):
"""
.. deprecated:: 1.0
Retrieve a reservation ID for a device reservation.
The reservation ID is useful to have when requesting containers.
Args:
lease_ref (str): The ID or name of the lease.
count (int): An optional count of devices the desired reservation was
made for. Use this if you have multiple reservations under a lease.
machine_name (str): An optional device machine name the desired reservation
was made for. Use this if you have multiple reservations under a lease.
device_model (str): An optional device model the desired reservation was
made for. Use this if you have multiple reservations under a lease.
device_name (str): An optional device name the desired reservation was
made for. Use this if you have multiple reservations under a lease.
Returns:
The ID of the reservation, if found.
Raises:
ValueError: If no reservation was found, or multiple were found.
"""
def _find_device_reservation(res):
if res.get("resource_type") != "device":
return False
# FIXME(jason): Blazar's device plugin uses "min" and "max", but the
# standard seems to be "min_count" and "max_count"; this should be fixed in
# Blazar's device plugin.
if count is not None and not all(
(key not in res) or int(res.get(key)) == count
for key in ["min_count", "max_count", "min", "max"]
):
return False
resource_properties = res.get("resource_properties")
if machine_name is not None and machine_name not in resource_properties:
return False
if device_model is not None and device_model not in resource_properties:
return False
if device_name is not None and device_name not in resource_properties:
return False
return True
res = _reservation_matching(lease_ref, _find_device_reservation)
return res["id"]
[docs]
def get_reserved_floating_ips(lease_ref) -> "list[str]":
"""
.. deprecated:: 1.0
Get a list of Floating IP addresses reserved in a lease.
Args:
lease_ref (str): The ID or name of the lease.
Returns:
A list of all reserved Floating IP addresses, if any were reserved.
"""
def _find_fip_reservation(res):
return res.get("resource_type") == "virtual:floatingip"
res = _reservation_matching(lease_ref, _find_fip_reservation, multiple=True)
fips = list_floating_ips()
return [
fip["floating_ip_address"]
for fip in fips
if any(f"reservation:{r['id']}" in fip["tags"] for r in res)
]
def _reservation_matching(lease_ref, match_fn, multiple=False):
lease = get_lease(lease_ref)
reservations = lease.get("reservations", [])
if isinstance(reservations, str):
LOG.info("Blazar returned nested JSON structure, unpacking.")
try:
reservations = json.loads(reservations)
except Exception as e:
LOG.error(f"Error loading json data: {e}")
matches = [r for r in reservations if match_fn(r)]
if not matches:
raise ResourceError("No matching reservation found")
if multiple:
return matches
else:
if len(matches) > 1:
raise ResourceError("Multiple matching reservations found")
return matches[0]
[docs]
def add_network_reservation(
reservation_list,
network_name,
usage_type=None,
of_controller_ip=None,
of_controller_port=None,
vswitch_name=None,
stitch_provider=None,
resource_properties=None,
physical_network="physnet1",
):
"""
.. deprecated:: 1.0
Add a network reservation to a reservation list.
Args:
reservation_list (list[dict]): The list of reservations to add to.
The list will be extended in-place.
network_name (str): The name of the network to create when the
reservation starts.
of_controller_ip (str): The OpenFlow controller IP, if the network
should be controlled by an external controller.
of_controller_port (int): The OpenFlow controller port.
vswitch_name (str): The name of the virtual switch associated with
this network. See `the virtual forwarding context documentation
<https://chameleoncloud.readthedocs.io/en/latest/technical/networks/networks_sdn.html#corsa-dp2000-virtual-forwarding-contexts-network-layout-and-advanced-features>`_
for more details.
stich_provider (str): specify a stitching provider such as fabric. '
resource_properties (list): A list of resource property constraints. These take
the form [<operation>, <search_key>, <search_value>]
physical_network (str): The physical provider network to reserve from.
This only needs to be changed if you are reserving a `stitchable
network <https://chameleoncloud.readthedocs.io/en/latest/technical/networks/networks_stitching.html>`_.
(Default "physnet1").
"""
desc_parts = []
if of_controller_ip and of_controller_port:
desc_parts.append(f"OFController={of_controller_ip}:{of_controller_port}")
if vswitch_name:
desc_parts.append(f"VSwitchName={vswitch_name}")
user_constraints = (resource_properties or []).copy()
extra_constraints = []
if physical_network:
extra_constraints.append(["==", "$physical_network", physical_network])
if stitch_provider and stitch_provider != "fabric":
extra_constraints.append(["==", "$stitch_provider", stitch_provider])
else:
raise CHIValueError("stitch_provider must be 'fabric' or None")
if usage_type and usage_type != "storage":
extra_constraints.append(["==", "$usage_type", usage_type])
else:
raise CHIValueError("usage_type must be 'storage' or None")
resource_properties = _format_resource_properties(
user_constraints, extra_constraints
)
reservation_list.append(
{
"resource_type": "network",
"network_name": network_name,
"network_description": ",".join(desc_parts),
"resource_properties": json.dumps(resource_properties),
"network_properties": "",
}
)
[docs]
def add_fip_reservation(reservation_list, count=1):
"""
.. deprecated:: 1.0
Add a floating IP reservation to a reservation list.
Args:
reservation_list (list[dict]): The list of reservations to add to.
The list will be extended in-place.
count (int): The number of floating IPs to reserve.
"""
reservation_list.append(
{
"resource_type": "virtual:floatingip",
"network_id": get_network_id(PUBLIC_NETWORK),
"amount": count,
}
)
[docs]
def add_device_reservation(
reservation_list, count=1, machine_name=None, device_model=None, device_name=None
):
"""
.. deprecated:: 1.0
Add an IoT/edge device reservation to a reservation list.
Args:
reservation_list (list[dict]): The list of reservations to add to.
count (int): The number of devices to request.
machine_name (str): The device machine name to reserve. This should match
a "machine_name" property of the devices registered in Blazar. This
is the easiest way to reserve a particular device type, e.g.
"raspberrypi4-64".
device_model (str): The model of device to reserve. This should match
a "model" property of the devices registered in Blazar.
device_name (str): The name of a specific device to reserve. If this
is provided in conjunction with ``count`` or other constraints,
an error will be raised, as there is only 1 possible device that
can match this criteria, because devices have unique names.
Raises:
ValueError: If ``device_name`` is provided, but ``count`` is greater
than 1, or some other constraint is present.
"""
reservation = {
"resource_type": "device",
"min": count,
"max": count,
}
resource_properties = []
if device_name:
if count > 1:
raise ResourceError(
"Cannot reserve multiple devices if device_name is a constraint."
)
resource_properties.append(["==", "$name", device_name])
if machine_name:
resource_properties.append(["==", "$machine_name", machine_name])
if device_model:
resource_properties.append(["==", "$model", device_model])
if len(resource_properties) == 1:
resource_properties = resource_properties[0]
elif resource_properties:
resource_properties.insert(0, "and")
reservation["resource_properties"] = json.dumps(resource_properties)
reservation_list.append(reservation)
[docs]
def lease_duration(days=1, hours=0, td=None):
"""
Compute the start and end dates for a lease given its desired duration.
When providing both ``days`` and ``hours``, the duration is summed. So,
the following would be a lease for one and a half days:
.. code-block:: python
start_date, end_date = lease_duration(days=1, hours=12)
Args:
days (int): The number of days the lease should be for.
hours (int): The number of hours the lease should be for.
"""
now = utcnow()
# Start one minute into future to avoid Blazar thinking lease is in past
# due to rounding to closest minute.
start_date = (now + timedelta(minutes=1)).strftime(BLAZAR_TIME_FORMAT)
end_date = (now + timedelta(days=days, hours=hours)).strftime(BLAZAR_TIME_FORMAT)
return start_date, end_date
#########
# Leases
#########
[docs]
def list_leases() -> List[Lease]:
"""
Return a list of user leases.
Returns:
A list of Lease objects representing user leases.
"""
blazar_client = blazar()
lease_dicts = blazar_client.lease.list()
leases = []
for lease_dict in lease_dicts:
lease = Lease(lease_json=lease_dict)
leases.append(lease)
return leases
def _get_lease_from_blazar(ref: str):
blazar_client = blazar()
try:
lease_dict = blazar_client.lease.get(ref)
return lease_dict
except BlazarClientException as err:
# Blazar's exception class is a bit odd and stores the actual code
# in 'kwargs'. The 'code' attribute on the exception is just the default
# code. Prefer to use .kwargs['code'] if present, fall back to .code
code = getattr(err, "kwargs", {}).get("code", getattr(err, "code", None))
if code == 404:
try:
lease_id = get_lease_id(ref)
lease_dict = blazar_client.lease.get(lease_id)
return lease_dict
except Exception:
# If we still can't find the lease, return None
return None
else:
raise
[docs]
def get_lease(ref: str) -> Union[Lease, None]:
"""
Get a lease by its ID or name.
Args:
ref (str): The ID or name of the lease.
Returns:
A Lease object matching the ID or name, or None if not found.
"""
if Version(context.version) >= Version("1.0"):
blazar_lease = _get_lease_from_blazar(ref)
if blazar_lease is None:
raise CHIValueError(f"Lease not found maching {ref}")
return Lease(lease_json=blazar_lease)
try:
return blazar().lease.get(ref)
except BlazarClientException as err:
# Blazar's exception class is a bit odd and stores the actual code
# in 'kwargs'. The 'code' attribute on the exception is just the default
# code. Prefer to use .kwargs['code'] if present, fall back to .code
code = getattr(err, "kwargs", {}).get("code", getattr(err, "code", None))
if code == 404:
return blazar().lease.get(get_lease_id(ref))
[docs]
def get_lease_id(lease_name) -> str:
"""Look up a lease's ID from its name.
Args:
name (str): The name of the lease.
Returns:
The ID of the found lease.
Raises:
ValueError: If the lease could not be found, or if multiple leases were
found with the same name.
"""
matching = [lease for lease in blazar().lease.list() if lease["name"] == lease_name]
if not matching:
raise CHIValueError(f"No leases found for name {lease_name}")
elif len(matching) > 1:
raise ResourceError(f"Multiple leases found for name {lease_name}")
return matching[0]["id"]
[docs]
def create_lease(lease_name, reservations=[], start_date=None, end_date=None):
"""
.. deprecated:: 1.0
Create a new lease with some requested reservations.
Args:
lease_name (str): The name to give the new lease.
reservations (list[dict]): The reservations to request with the lease.
start_date (datetime): The start date of the lease. (Defaults to now.)
end_date (datetime): The end date of the lease. (Defaults to 1 day from
the lease start date.)
Returns:
The created lease representation.
"""
if not (start_date or end_date):
start_date, end_date = lease_duration(days=1)
elif not end_date:
end_date = start_date + timedelta(days=1)
elif not start_date:
start_date = utcnow()
if not reservations:
raise CHIValueError("No reservations provided.")
try:
return blazar().lease.create(
name=lease_name,
start=start_date,
end=end_date,
reservations=reservations,
events=[],
)
except BlazarClientException as ex:
msg: "str" = ex.args[0]
msg = msg.lower()
match = ErrorParsers.NOT_ENOUGH_RESOURCES.match(msg)
if match:
LOG.error(
f"There were not enough unreserved {match.group('resource_type')} "
"to satisfy your request."
)
else:
LOG.error(msg)
[docs]
def delete_lease(ref):
"""
.. deprecated:: 1.0
Delete the lease.
Args:
ref (str): The name or ID of the lease.
"""
lease = get_lease(ref)
lease.delete()
print(f"Deleted lease {ref}")
[docs]
def wait_for_active(ref):
"""
.. deprecated:: 1.0
Wait for the lease to become active.
This function will wait for 2.5 minutes, which is a somewhat arbitrary
amount of time.
Args:
ref (str): The name or ID of the lease.
Returns:
The lease in ACTIVE state.
Raises:
TimeoutError: If the lease fails to become active within the timeout.
"""
for _ in range(15):
lease = get_lease(ref)
status = lease["status"]
if status == "ACTIVE":
return lease
elif status == "ERROR":
raise ServiceError("Lease went into ERROR state")
time.sleep(10)
raise ServiceError("Lease failed to start")