Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions chi/context.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
import re
import sys
import time
from typing import List, Optional
Expand Down Expand Up @@ -72,6 +73,7 @@ def default_key_name():
_auth_plugin = None
_session = None
_sites = {}
_lease_id = None

version = "1.1"

Expand Down Expand Up @@ -440,6 +442,70 @@ def on_change(change):
else:
print("Choose site feature is only available in an ipynb environment.")

def use_lease_id(lease_id: str) -> None:
"""
Sets the current lease ID to use in the global context.

This configures the lease so it can be stored for ease
of restoring suspended sessions. Further lease validation,
visualizations, and selectors are available in the lease module.

Args:
lease_id (str): The ID of the lease to use.
"""
global _lease_id

if not re.fullmatch(r"[A-Za-z0-9\-]+", lease_id):
raise CHIValueError(f'Lease ID "{lease_id}" is invalid. It must contain only letters, numbers, and hyphens with no spaces or special characters.')

_lease_id = lease_id

print(f"Now using lease with ID {lease_id}.")

def get_lease_id():
"""
Returns the currently active lease ID, if one has been set.

Returns:
str or None: The lease ID currently in use, or None if no lease has been selected.
"""
if _lease_id is None:
print("No lease ID has been set. Use `use_lease_id()` to select one.")
return _lease_id

def get_project_name(project_id: Optional[str] = None) -> str:
"""
Returns the name of a project by ID, or the current project name if no ID is given.

Args:
project_id (str, optional): The ID of the project. If None, uses the current session project.

Returns:
str: The name of the project.

Raises:
ResourceError: If the project cannot be found or the request fails.
"""
keystone_session = session()
keystone_client = KeystoneClient(
session=keystone_session,
interface=getattr(keystone_session, "interface", None),
region_name=getattr(keystone_session, "region_name", None),
)

try:
if project_id:
project = keystone_client.projects.get(project_id)
else:
current_id = keystone_session.get_project_id()
project = keystone_client.projects.get(current_id)
except keystone_exceptions.NotFound:
raise ResourceError("Project not found.")
except keystone_exceptions.Unauthorized:
raise ResourceError("Failed to retrieve project. Check your credentials.")

return project.name


def list_projects(show: str = None) -> List[str]:
"""
Expand Down
251 changes: 248 additions & 3 deletions chi/lease.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import json
import logging
import numbers
import os
import re
import time
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, List, Optional, Union

import pandas
from blazarclient.exception import BlazarClientException
from ipydatagrid import DataGrid, Expr, TextRenderer
from IPython.display import display
from ipywidgets import HTML
from ipywidgets import HTML, Box, Layout
from packaging.version import Version

from chi import context, server, util

from .clients import blazar
from .context import _is_ipynb
from .clients import blazar, connection
from .context import _is_ipynb, get_project_name
from .exception import CHIValueError, ResourceError, ServiceError
from .hardware import Device, Node
from .network import PUBLIC_NETWORK, get_network_id, list_floating_ips
Expand Down Expand Up @@ -259,6 +262,135 @@ def _populate_from_json(self, lease_json):

# self.events = lease_json.get('events', [])

def _ipython_display_(self):
"""
Displays a styled summary of the lease when run in a Jupyter notebook.

This method is called automatically by the Jupyter display system when
an instance of the Lease object is the final expression in a cell.
It presents key lease attributes using ipywidgets for readability.
"""
layout = Layout(padding="4px 10px")
style = {
"description_width": "initial",
"background": "#d3d3d3",
"white_space": "nowrap",
}

status_style = style.copy()
status_colors = {
"ACTIVE": "#a2d9fe",
"PENDING": "#ffe599",
"TERMINATED": "#f69084",
}
if self.status:
status_style["background"] = status_colors.get(self.status, "#d3d3d3")

children = [
# HTML(f"<b>Lease ID:</b> {self.id}", style=style, layout=layout),
HTML(f"<b>Status:</b> {self.status}", style=status_style, layout=layout),
HTML(f"<b>Name:</b> {self.name}", style=style, layout=layout),
]

if self.start_date:
children.append(
HTML(
f"<b>Start:</b> {self.start_date.strftime('%Y-%m-%d %H:%M')}",
style=style,
layout=layout,
)
)
if self.end_date:
children.append(
HTML(
f"<b>End:</b> {self.end_date.strftime('%Y-%m-%d %H:%M')}",
style=style,
layout=layout,
)
)

remaining = None
if self.end_date and datetime.now() < self.end_date:
remaining = self.end_date - datetime.now()

if remaining:
days = remaining.days
hours = remaining.seconds // 3600
minutes = (remaining.seconds % 3600) // 60
children.append(
HTML(
f"<b>Remaining:</b> {days:02d}d {hours:02d}h {minutes:02d}m",
style=style,
layout=layout,
)
)

# Reservations
children.append(
HTML(
f"<b>Node Reservations:</b> {len(self.node_reservations)}",
style=style,
layout=layout,
)
)
children.append(
HTML(
f"<b>FIP Reservations:</b> {len(self.fip_reservations)}",
style=style,
layout=layout,
)
)
children.append(
HTML(
f"<b>Device Reservations:</b> {len(self.device_reservations)}",
style=style,
layout=layout,
)
)

if self.project_id:
try:
project_name = context.get_project_name(self.project_id)
children.append(
HTML(
f"<b>Project Name:</b> {project_name}",
style=style,
layout=layout,
)
)
except ResourceError:
children.append(
HTML(
f"<b>Project ID:</b> {self.project_id}",
style=style,
layout=layout,
)
)
if self.user_id:
user_id = connection().get_user_id()
if self.user_id == user_id:
label = os.getenv("OS_USERNAME")
children.append(
HTML(f"<b>User Name:</b> {label}", style=style, layout=layout)
)
else:
label = self.user_id # [:8] # or just show a truncated ID
children.append(
HTML(f"<b>User ID:</b> {label}", style=style, layout=layout)
)
if self.created_at:
children.append(
HTML(
f"<b>Created At:</b> {self.created_at.strftime('%Y-%m-%d %H:%M')}",
style=style,
layout=layout,
)
)

box = Box(children=children)
box.layout = Layout(flex_flow="row wrap")
display(box)

def add_device_reservation(
self,
amount: int = None,
Expand Down Expand Up @@ -1078,6 +1210,119 @@ def list_leases() -> List[Lease]:

return leases

def _status_color(cell):
return "#a2d9fe" if cell.value == "2-ACTIVE" else (
"#ffe599" if cell.value == "1-PENDING" else (
"#f69084" if cell.value == "3-TERMINATED" else "#e0e0e0"))

def show_leases() -> DataGrid:
"""
Displays a table of the user's leases in an interactive, sortable format.

Uses an ipydatagrid to present key lease attributes such as ID, name, status,
duration, and reservation counts. The grid supports sorting, filtering, and
scrolling for easy exploration of lease state.

Returns:
DataGrid: An ipydatagrid widget displaying the leases.
"""

def estimate_column_width(df, column, char_px=7, padding=0):
if column not in df.columns:
raise ValueError(f"Column '{column}' not found in DataFrame.")
max_chars = df[column].astype(str).map(len).max()
return max(max_chars * char_px + padding, 80)


leases = list_leases()

rows = []
for lease in leases:

try:
project_name = get_project_name(lease.project_id)
except ResourceError:
project_name = lease.project_id[:8] if lease.project_id else "Unknown"

if lease.user_id == connection().current_user_id:
user_label = os.getenv("OS_USERNAME")
else:
user_label = lease.user_id if lease.user_id else "Unknown"

if lease.start_date and lease.end_date:
duration_hrs = round((lease.end_date - lease.start_date).total_seconds() / 3600, 1)
else:
duration_hrs = "N/A"

# Inside your row-building loop:
if lease.end_date and lease.end_date > datetime.now():
remaining_td = lease.end_date - datetime.now()
remaining_str = f"{remaining_td.days:02d}d {(remaining_td.seconds // 3600):02d}h"
elif lease.end_date and lease.end_date <= datetime.now():
remaining_str = "Expired"
else:
remaining_str = "N/A"


# prepending status with numeric makes it possible to character sort
# since ipydatagrid does not allow custom sort functions
status_order = {
"PENDING": "1-PENDING",
"ACTIVE": "2-ACTIVE",
"TERMINATED": "3-TERMINATED"
}

rows.append({
"Name": lease.name,
"Status": status_order.get(lease.status, f"4-{lease.status}"),
"User": user_label,
"Project": project_name,
"Start": lease.start_date.strftime("%Y-%m-%d %H:%M") if lease.start_date else "",
"End": lease.end_date.strftime("%Y-%m-%d %H:%M") if lease.end_date else "",
"Remaining": remaining_str,
"Total Hours": duration_hrs,
"# Nodes": len(lease.node_reservations),
"# FIPs": len(lease.fip_reservations),
"Created": lease.created_at.strftime("%Y-%m-%d %H:%M") if lease.created_at else "",
"Lease ID": lease.id,
"_is_user_lease": 0 if lease.user_id == connection().current_user_id else 1
})

df = pandas.DataFrame(rows)
df = pandas.DataFrame(rows)
df = df.sort_values(by=["_is_user_lease", "Status", "Created"])
df = df.drop(columns=["_is_user_lease"])

renderers = {
"Status": TextRenderer(
background_color=Expr(_status_color),
text_color="black",
),

}

display(DataGrid(
df,
layout={"height": "400px", "width": "100%"},
column_widths={
"key": 30,
"Name": int(estimate_column_width(df, "Name")),
"Status": 120,
"Remaining": 80,
"Total Hours": 50,
"# Nodes": 30,
"# FIPs": 30,
"Project": 100,
"User": 75,
"Start": 95,
"End": 95,
"Created": 95,
"Lease ID": 30,

},
renderers=renderers
))


def _get_lease_from_blazar(ref: str):
blazar_client = blazar()
Expand Down
Loading