diff --git a/chi/lease.py b/chi/lease.py index 9855d3a..566d874 100644 --- a/chi/lease.py +++ b/chi/lease.py @@ -269,7 +269,19 @@ def add_node_reservation(self, 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") @@ -283,6 +295,15 @@ def add_node_reservation(self, node_name=node_name) 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) @@ -290,6 +311,14 @@ 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, @@ -300,6 +329,21 @@ def submit(self, wait_timeout: int = 300, show: List[str] = ["widget", "text"], 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 (List[str], optional): The types of lease information to display. Defaults to ["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 = self._get_existing_lease() if existing_lease: diff --git a/chi/server.py b/chi/server.py index 5e2f6e1..c5d7914 100644 --- a/chi/server.py +++ b/chi/server.py @@ -97,21 +97,54 @@ def instance_create_args( return server_args class Server: + """ + Represents an instance. + + Args: + name (str): The name of the server. + reservation_id (Optional[str]): The reservation ID associated with the server. Defaults to None. + image_name (str): The name of the image to use for the server. Defaults to DEFAULT_IMAGE_NAME. + image (Optional[str]): The image ID or name to use for the server. Defaults to None. + flavor_name (str): The name of the flavor to use for the server. Defaults to BAREMETAL_FLAVOR. + key_name (str): The name of the keypair to use for the server. Defaults to None. + keypair (Optional[Keypair]): The keypair object to use for the server. Defaults to None. + network_name (str): The name of the network to use for the server. Defaults to DEFAULT_NETWORK. + + Attributes: + name (str): The name of the server. + reservation_id (Optional[str]): The reservation ID associated with the server. + image_name (str): The name of the image used for the server. + flavor_name (str): The name of the flavor used for the server. + keypair (Optional[Keypair]): The keypair object used for the server. + network_name (str): The name of the network used for the server. + id (Optional[str]): The ID of the server. + _addresses (Dict[str, List[str]]): The IP addresses associated with the server. + created_at (Optional[datetime]): The timestamp when the server was created. + host_id (Optional[str]): The ID of the host where the server is running. + host_status (Optional[str]): The status of the host where the server is running. + hypervisor_hostname (Optional[str]): The hostname of the hypervisor where the server is running. + is_locked (bool): Indicates whether the server is locked. + _status (Optional[str]): The status of the server. + fault (Optional[dict]): The fault information of the server. + """ + def __init__(self, name: str, reservation_id: Optional[str] = None, image_name: str = DEFAULT_IMAGE_NAME, image: Optional[str] = None, flavor_name: str = BAREMETAL_FLAVOR, key_name: str = None, keypair: Optional[Keypair] = None, network_name: str = DEFAULT_NETWORK): - - self.name = name self.reservation_id = reservation_id or None + # Add this once chi.image is implemented # self.image = image or get_image(image_name) self.image_name = image_name self.flavor_name = flavor_name - ## Add an update keypair call before this and make sure this does not crash on init - update_keypair() - self.keypair = keypair or get_keypair(key_name or f"{os.environ.get('USER')}-jupyter") + if keypair: + self.keypair = keypair + else: + update_keypair() + self.keypair = get_keypair(key_name or get_from_context("keypair_name")) + self.network_name = network_name self.conn = connection(session=session()) @@ -124,38 +157,36 @@ def __init__(self, name: str, reservation_id: Optional[str] = None, image_name: self.hypervisor_hostname: Optional[str] = None self.is_locked: bool = False self._status: Optional[str] = None + self.fault: Optional[dict] = None @property def addresses(self) -> Dict[str, List[str]]: if self.id: - self._refresh() + self.refresh() return self._addresses @property def status(self) -> Optional[str]: if self.id: - self._refresh() + self.refresh() return self._status - def _refresh(self): - nova_server = nova().servers.get(get_server_id(self.name)) - conn_server = self.conn.compute.get_server(get_server_id(self.name)) + def submit(self, wait_for_active: bool = True, show: str = "widget", + idempotent: bool = False) -> 'Server': + """ + Submits a server creation request to the Nova API. - try: - self.id = nova_server.id - self._status = nova_server.status - self._addresses = nova_server.addresses - self.created_at = nova_server.created - self.host_id = nova_server.hostId - self.host_status = conn_server.host_status - self.hypervisor_hostname = conn_server.hypervisor_hostname - self.is_locked = conn_server.is_locked - except Exception: - raise ResourceError("Could not refresh server") + Args: + wait_for_active (bool, optional): Whether to wait for the server to become active before returning. Defaults to True. + show (str, optional): The type of server information to display after creation. Defaults to "widget". + idempotent (bool, optional): Whether to create the server only if it doesn't already exist. Defaults to False. + Returns: + Server: The created server object. - def submit(self, wait_for_active: bool = True, wait_timeout: int = 720, - show: str = "widget", idempotent: bool = False) -> 'Server': + Raises: + Conflict: If the server creation fails due to a conflict and idempotent mode is not enabled. + """ nova_client = nova() if idempotent: @@ -209,16 +240,16 @@ def _from_nova_server(cls, nova_server): flavor_id = nova_server.flavor_id try: - network_id = list(nova_server.networks.keys())[0] + network_id = list(nova_server.networks.keys())[0] if len(nova_server.networks) > 0 else None except Exception: - network_id = nova_server.networks[0]['uuid'] + network_id = nova_server.networks[0]['uuid'] if len(nova_server.networks) > 0 else None server = cls(name=nova_server.name, reservation_id=None, - image_name = get_image(image_id).name, + image_name=get_image(image_id).name, flavor_name=get_flavor(flavor_id).name, key_name=nova_server.key_name, - network_name=get_network(network_id)['name']) + network_name=get_network(network_id)['name'] if network_id is not None else None ) try: created_at = nova_server.created @@ -253,24 +284,75 @@ def _from_nova_server(cls, nova_server): server.host_status = host_status server.hypervisor_hostname = hypervisor_hostname server.is_locked = is_locked + server.fault = connection(session()).compute.get_server(get_server_id(server.name)).fault return server def delete(self) -> None: delete_server(self.id) + def refresh(self): + """ + Refreshes the server's information by retrieving the latest details from the server provider. + + Raises: + ResourceError: If the server refresh fails. + """ + try: + nova_server = nova().servers.get(get_server_id(self.name)) + conn_server = self.conn.compute.get_server(get_server_id(self.name)) + + + self.id = nova_server.id + self._status = nova_server.status + self._addresses = nova_server.addresses + self.created_at = nova_server.created + self.host_id = nova_server.hostId + self.host_status = conn_server.host_status + self.hypervisor_hostname = conn_server.hypervisor_hostname + self.is_locked = conn_server.is_locked + self.fault = conn_server.fault + except Exception as e: + raise ResourceError(f"Could not refresh server: {e}") + def wait(self, status: str = "ACTIVE") -> None: - print(f"Waiting for server {self.name}'s status to turn to {status}. This can take up to 12 minutes") - timeout = 720 # 12 minutes + """ + Waits for the server's status to reach the specified status. + + Args: + status (str): The status to wait for. Defaults to "ACTIVE". + + Raises: + ServiceError: If the server does not reach the specified status within the timeout period. + + Returns: + None + """ + print(f"Waiting for server {self.name}'s status to turn to {status}. This can take up to 18 minutes") + timeout = 1800 # 12 minutes start_time = time.time() while time.time() - start_time < timeout: - self._refresh() - if self.status == status.upper(): + self.refresh() + if self.status == status.upper() or self.status == "ERROR": + print(f"Server has moved to status {self.status}") return time.sleep(5) # Wait for 5 seconds before checking again raise ServiceError(f"Timeout waiting for server to reach {status} status") def show(self, type: str = "text", wait_for_active: bool = False) -> None: + """ + Display the content of the server. + + Args: + type (str, optional): The type of content to display. options are ["text","widget"]. Defaults to "text". + wait_for_active (bool, optional): Whether to wait for the server to be active before displaying the content. Defaults to False. + + Raises: + CHIValueError: If an invalid show type is provided. + + Returns: + None + """ if wait_for_active: self.wait("ACTIVE") @@ -296,6 +378,7 @@ def _show_text(self, server): print(f" Host Status: {server.host_status}") print(f" Hypervisor Hostname: {server.hypervisor_hostname}") print(f" Is Locked: {server.is_locked}") + print(f" Fault: {server.fault}") def _show_widget(self, server): html = "