python-chi 1.2.2__tar.gz → 1.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_chi-1.2.3/ChangeLog +7 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/PKG-INFO +3 -1
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/container.py +9 -1
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/context.py +71 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/hardware.py +183 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/lease.py +267 -3
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/network.py +0 -43
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/server.py +0 -1
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/share.py +0 -11
- {python_chi-1.2.2 → python_chi-1.2.3}/python_chi.egg-info/PKG-INFO +3 -1
- python_chi-1.2.3/python_chi.egg-info/pbr.json +1 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/python_chi.egg-info/requires.txt +2 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/requirements.txt +3 -0
- python_chi-1.2.3/setup.py +5 -0
- python_chi-1.2.2/ChangeLog +0 -7
- python_chi-1.2.2/python_chi.egg-info/pbr.json +0 -1
- python_chi-1.2.2/setup.py +0 -5
- {python_chi-1.2.2 → python_chi-1.2.3}/.github/CODEOWNERS +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/.github/workflows/linting.yml +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/.github/workflows/pypi-publish.yml +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/.github/workflows/test.yml +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/.mailmap +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/.readthedocs.yml +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/AUTHORS +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/DEVELOPMENT.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/LICENSE +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/Makefile +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/README.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/__init__.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/clients.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/exception.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/image.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/jupyterhub.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/keypair.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/magic.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/ssh.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/storage.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/chi/util.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/__init__.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/_templates/page.html +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/conf.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/examples.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/generate_notebook.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/index.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/clients.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/container.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/context.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/exception.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/hardware.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/image.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/lease.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/magic.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/network.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/server.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/share.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/ssh.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/storage.rst +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/docs/requirements.txt +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/python_chi.egg-info/SOURCES.txt +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/python_chi.egg-info/dependency_links.txt +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/python_chi.egg-info/not-zip-safe +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/python_chi.egg-info/top_level.txt +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/ruff.toml +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/setup.cfg +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/test-requirements.txt +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/tests/__init__.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/tests/test_container.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/tests/test_context.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/tests/test_lease.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/tests/test_network.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/tests/test_server.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/tests/test_share.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/tests/test_ssh.py +0 -0
- {python_chi-1.2.2 → python_chi-1.2.3}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-chi
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Helper library for Chameleon Infrastructure (CHI) testbed
|
|
5
5
|
Home-page: https://www.chameleoncloud.org
|
|
6
6
|
Author: University of Chicago
|
|
@@ -27,9 +27,11 @@ Requires-Dist: python-novaclient
|
|
|
27
27
|
Requires-Dist: python-swiftclient
|
|
28
28
|
Requires-Dist: python-zunclient
|
|
29
29
|
Requires-Dist: ipython
|
|
30
|
+
Requires-Dist: ipydatagrid
|
|
30
31
|
Requires-Dist: ipywidgets
|
|
31
32
|
Requires-Dist: networkx
|
|
32
33
|
Requires-Dist: matplotlib
|
|
34
|
+
Requires-Dist: pandas
|
|
33
35
|
Dynamic: author
|
|
34
36
|
Dynamic: author-email
|
|
35
37
|
Dynamic: classifier
|
|
@@ -17,7 +17,7 @@ import logging
|
|
|
17
17
|
import os
|
|
18
18
|
import tarfile
|
|
19
19
|
import time
|
|
20
|
-
from typing import List, Optional, Tuple
|
|
20
|
+
from typing import Dict, List, Optional, Tuple
|
|
21
21
|
|
|
22
22
|
from IPython.display import HTML, display
|
|
23
23
|
from packaging.version import Version
|
|
@@ -62,6 +62,8 @@ class Container:
|
|
|
62
62
|
id (str): The ID of the container.
|
|
63
63
|
created_at (str): The timestamp when the container was created.
|
|
64
64
|
status (str): The current status of the container.
|
|
65
|
+
environment (Dict[str, str]): A dictionary of environment variables for the container.
|
|
66
|
+
device_profiles (List[str]): A list of device profiles to be configured on the container.
|
|
65
67
|
"""
|
|
66
68
|
|
|
67
69
|
def __init__(
|
|
@@ -75,6 +77,8 @@ class Container:
|
|
|
75
77
|
runtime: str = None,
|
|
76
78
|
command: List[str] = None,
|
|
77
79
|
workdir: str = None,
|
|
80
|
+
environment: Dict[str, str] = {},
|
|
81
|
+
device_profiles: List[str] = [],
|
|
78
82
|
):
|
|
79
83
|
self.name = name
|
|
80
84
|
self.image_ref = image_ref
|
|
@@ -88,6 +92,8 @@ class Container:
|
|
|
88
92
|
self._status = None
|
|
89
93
|
self.command = command
|
|
90
94
|
self.workdir = workdir
|
|
95
|
+
self.environment = environment
|
|
96
|
+
self.device_profiles = device_profiles
|
|
91
97
|
|
|
92
98
|
@classmethod
|
|
93
99
|
def from_zun_container(cls, zun_container):
|
|
@@ -152,6 +158,8 @@ class Container:
|
|
|
152
158
|
start=self.start,
|
|
153
159
|
start_timeout=self.start_timeout,
|
|
154
160
|
runtime=self.runtime,
|
|
161
|
+
environment=self.environment,
|
|
162
|
+
device_profiles=self.device_profiles,
|
|
155
163
|
**kwargs,
|
|
156
164
|
)
|
|
157
165
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
+
import re
|
|
3
4
|
import sys
|
|
4
5
|
import time
|
|
5
6
|
from typing import List, Optional
|
|
@@ -72,6 +73,7 @@ deprecated_extra_opts = {
|
|
|
72
73
|
_auth_plugin = None
|
|
73
74
|
_session = None
|
|
74
75
|
_sites = {}
|
|
76
|
+
_lease_id = None
|
|
75
77
|
|
|
76
78
|
version = "1.1"
|
|
77
79
|
|
|
@@ -441,6 +443,75 @@ def choose_site(default: str = None) -> None:
|
|
|
441
443
|
print("Choose site feature is only available in an ipynb environment.")
|
|
442
444
|
|
|
443
445
|
|
|
446
|
+
def use_lease_id(lease_id: str) -> None:
|
|
447
|
+
"""
|
|
448
|
+
Sets the current lease ID to use in the global context.
|
|
449
|
+
|
|
450
|
+
This configures the lease so it can be stored for ease
|
|
451
|
+
of restoring suspended sessions. Further lease validation,
|
|
452
|
+
visualizations, and selectors are available in the lease module.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
lease_id (str): The ID of the lease to use.
|
|
456
|
+
"""
|
|
457
|
+
global _lease_id
|
|
458
|
+
|
|
459
|
+
if not re.fullmatch(r"[A-Za-z0-9\-]+", lease_id):
|
|
460
|
+
raise CHIValueError(
|
|
461
|
+
f'Lease ID "{lease_id}" is invalid. It must contain only letters, numbers, and hyphens with no spaces or special characters.'
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
_lease_id = lease_id
|
|
465
|
+
|
|
466
|
+
print(f"Now using lease with ID {lease_id}.")
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def get_lease_id():
|
|
470
|
+
"""
|
|
471
|
+
Returns the currently active lease ID, if one has been set.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
str or None: The lease ID currently in use, or None if no lease has been selected.
|
|
475
|
+
"""
|
|
476
|
+
if _lease_id is None:
|
|
477
|
+
print("No lease ID has been set. Use `use_lease_id()` to select one.")
|
|
478
|
+
return _lease_id
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def get_project_name(project_id: Optional[str] = None) -> str:
|
|
482
|
+
"""
|
|
483
|
+
Returns the name of a project by ID, or the current project name if no ID is given.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
project_id (str, optional): The ID of the project. If None, uses the current session project.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
str: The name of the project.
|
|
490
|
+
|
|
491
|
+
Raises:
|
|
492
|
+
ResourceError: If the project cannot be found or the request fails.
|
|
493
|
+
"""
|
|
494
|
+
keystone_session = session()
|
|
495
|
+
keystone_client = KeystoneClient(
|
|
496
|
+
session=keystone_session,
|
|
497
|
+
interface=getattr(keystone_session, "interface", None),
|
|
498
|
+
region_name=getattr(keystone_session, "region_name", None),
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
try:
|
|
502
|
+
if project_id:
|
|
503
|
+
project = keystone_client.projects.get(project_id)
|
|
504
|
+
else:
|
|
505
|
+
current_id = keystone_session.get_project_id()
|
|
506
|
+
project = keystone_client.projects.get(current_id)
|
|
507
|
+
except keystone_exceptions.NotFound:
|
|
508
|
+
raise ResourceError("Project not found.")
|
|
509
|
+
except keystone_exceptions.Unauthorized:
|
|
510
|
+
raise ResourceError("Failed to retrieve project. Check your credentials.")
|
|
511
|
+
|
|
512
|
+
return project.name
|
|
513
|
+
|
|
514
|
+
|
|
444
515
|
def list_projects(show: str = None) -> List[str]:
|
|
445
516
|
"""
|
|
446
517
|
Retrieves a list of projects associated with the current user.
|
|
@@ -5,7 +5,11 @@ from dataclasses import dataclass
|
|
|
5
5
|
from datetime import datetime, timedelta, timezone
|
|
6
6
|
from typing import List, Optional, Set, Tuple
|
|
7
7
|
|
|
8
|
+
import pandas as pd
|
|
8
9
|
import requests
|
|
10
|
+
from ipydatagrid import DataGrid, Expr, TextRenderer
|
|
11
|
+
from IPython.display import display
|
|
12
|
+
from ipywidgets import HTML, Box, Layout
|
|
9
13
|
|
|
10
14
|
from chi import exception
|
|
11
15
|
|
|
@@ -98,6 +102,101 @@ class Node:
|
|
|
98
102
|
blazarclient.host.get_allocation(host_id), minimum_hours
|
|
99
103
|
)
|
|
100
104
|
|
|
105
|
+
def _ipython_display_(self):
|
|
106
|
+
"""
|
|
107
|
+
Displays information about the node. This function is called passively by the Jupyter display system.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
layout = Layout(padding="4px 10px")
|
|
111
|
+
style = {
|
|
112
|
+
"description_width": "initial",
|
|
113
|
+
"background": "#d3d3d3",
|
|
114
|
+
"white_space": "nowrap",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
reservable_style = {
|
|
118
|
+
"description_width": "initial",
|
|
119
|
+
"background": " #a2d9fe",
|
|
120
|
+
"white_space": "nowrap",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if not self.reservable:
|
|
124
|
+
reservable_style["background"] = "#f69084"
|
|
125
|
+
|
|
126
|
+
children = [
|
|
127
|
+
HTML(f"<b>Node Name:</b> {self.name}", style=style, layout=layout),
|
|
128
|
+
HTML(f"<b>Site:</b> {self.site}", style=style, layout=layout),
|
|
129
|
+
HTML(f"<b>Type:</b> {self.type}", style=style, layout=layout),
|
|
130
|
+
]
|
|
131
|
+
if getattr(self, "cpu", False) and "clock_speed" in self.cpu:
|
|
132
|
+
children.append(
|
|
133
|
+
HTML(
|
|
134
|
+
f"<b>Clock Speed:</b> {self.cpu['clock_speed'] / 1e9:.2f} GHz",
|
|
135
|
+
style=style,
|
|
136
|
+
layout=layout,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
getattr(self, "main_memory", False)
|
|
142
|
+
and "humanized_ram_size" in self.main_memory
|
|
143
|
+
):
|
|
144
|
+
children.append(
|
|
145
|
+
HTML(
|
|
146
|
+
f"<b>RAM:</b> {self.main_memory['humanized_ram_size']}",
|
|
147
|
+
style=style,
|
|
148
|
+
layout=layout,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if getattr(self, "gpu", False) and "gpu" in self.gpu and self.gpu["gpu"]:
|
|
153
|
+
if "gpu_count" in self.gpu:
|
|
154
|
+
children.append(
|
|
155
|
+
HTML(
|
|
156
|
+
f"<b>GPU Count:</b> {self.gpu['gpu_count']}",
|
|
157
|
+
style=style,
|
|
158
|
+
layout=layout,
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
children.append(HTML("<b>GPU:</b> True", style=style, layout=layout))
|
|
163
|
+
if "gpu_model" in self.gpu:
|
|
164
|
+
children.append(
|
|
165
|
+
HTML(
|
|
166
|
+
f"<b>GPU Model:</b> {self.gpu['gpu_model']}",
|
|
167
|
+
style=style,
|
|
168
|
+
layout=layout,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
children.append(HTML("<b>GPU Count:</b> 0", style=style, layout=layout))
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
getattr(self, "storage_devices", False)
|
|
176
|
+
and len(self.storage_devices) > 0
|
|
177
|
+
and "humanized_size" in self.storage_devices[0]
|
|
178
|
+
):
|
|
179
|
+
children.append(
|
|
180
|
+
HTML(
|
|
181
|
+
f"<b>Storage Size:</b> {self.storage_devices[0]['humanized_size']}",
|
|
182
|
+
style=style,
|
|
183
|
+
layout=layout,
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if getattr(self, "reservable", False):
|
|
188
|
+
children.append(
|
|
189
|
+
HTML(
|
|
190
|
+
f"<b>Reservable:</b> {'Yes' if self.reservable else 'No'}",
|
|
191
|
+
style=reservable_style,
|
|
192
|
+
layout=layout,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
box = Box(children=children)
|
|
197
|
+
box.layout = Layout(flex_flow="row wrap")
|
|
198
|
+
display(box)
|
|
199
|
+
|
|
101
200
|
|
|
102
201
|
def _call_api(endpoint):
|
|
103
202
|
url = "{0}/{1}.{2}".format(RESOURCE_API_URL, endpoint, "json")
|
|
@@ -260,6 +359,90 @@ def get_node_types() -> List[str]:
|
|
|
260
359
|
return list(set(node_types))
|
|
261
360
|
|
|
262
361
|
|
|
362
|
+
def _reservable_color(cell):
|
|
363
|
+
return "#a2d9fe" if cell.value else "#f69084"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _gpu_background_color(cell):
|
|
367
|
+
return "#d3d3d3" if not cell.value else None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def show_nodes(nodes: Optional[List[Node]] = None) -> None:
|
|
371
|
+
"""
|
|
372
|
+
Display a sortable, filterable table of available nodes.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
nodes (Optional[List[Node]], optional): A list of Node objects to display.
|
|
376
|
+
If not provided, defaults to the output of hardware.get_nodes().
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
None
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
def estimate_column_width(df, column, char_px=7, padding=0):
|
|
383
|
+
if column not in df.columns:
|
|
384
|
+
raise ValueError(f"Column '{column}' not found in DataFrame.")
|
|
385
|
+
max_chars = df[column].astype(str).map(len).max()
|
|
386
|
+
return max(max_chars * char_px + padding, 80)
|
|
387
|
+
|
|
388
|
+
if not nodes:
|
|
389
|
+
nodes = get_nodes()
|
|
390
|
+
|
|
391
|
+
rows = []
|
|
392
|
+
for n in nodes:
|
|
393
|
+
rows.append(
|
|
394
|
+
{
|
|
395
|
+
"Node Name": n.name,
|
|
396
|
+
"Type": n.type,
|
|
397
|
+
"Clock Speed (GHz)": round(n.cpu.get("clock_speed", 0) / 1e9, 2),
|
|
398
|
+
"RAM": n.main_memory.get("humanized_ram_size", "N/A"),
|
|
399
|
+
"GPU Model": (n.gpu or {}).get("gpu_model") or "",
|
|
400
|
+
"GPU Count": (n.gpu or {}).get("gpu_count") or "",
|
|
401
|
+
"Storage Size": n.storage_devices[0].get("humanized_size", "N/A")
|
|
402
|
+
if n.storage_devices
|
|
403
|
+
else "N/A",
|
|
404
|
+
"Site": n.site,
|
|
405
|
+
"Reservable": bool(n.reservable),
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
df = pd.DataFrame(rows)
|
|
410
|
+
renderers = {
|
|
411
|
+
"Reservable": TextRenderer(
|
|
412
|
+
text_color="black",
|
|
413
|
+
background_color=Expr(_reservable_color),
|
|
414
|
+
),
|
|
415
|
+
"GPU Model": TextRenderer(
|
|
416
|
+
background_color=Expr(_gpu_background_color),
|
|
417
|
+
),
|
|
418
|
+
"GPU Count": TextRenderer(
|
|
419
|
+
background_color=Expr(_gpu_background_color),
|
|
420
|
+
),
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
grid = DataGrid(
|
|
424
|
+
df,
|
|
425
|
+
layout=Layout(height="400px"),
|
|
426
|
+
selection_mode="row",
|
|
427
|
+
renderers=renderers,
|
|
428
|
+
column_widths={
|
|
429
|
+
"Node Name": int(estimate_column_width(df, "Node Name")),
|
|
430
|
+
"Site": int(estimate_column_width(df, "Site")),
|
|
431
|
+
"Type": int(estimate_column_width(df, "Type")),
|
|
432
|
+
"RAM": int(estimate_column_width(df, "RAM")),
|
|
433
|
+
"Storage Size": int(estimate_column_width(df, "Storage Size")),
|
|
434
|
+
"Clock Speed (GHz)": 55,
|
|
435
|
+
"GPU Model": 90,
|
|
436
|
+
"GPU Count": 30,
|
|
437
|
+
"key": 30,
|
|
438
|
+
"Reservable": 55,
|
|
439
|
+
},
|
|
440
|
+
df=pd.DataFrame(rows),
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
display(grid)
|
|
444
|
+
|
|
445
|
+
|
|
263
446
|
@dataclass
|
|
264
447
|
class Device:
|
|
265
448
|
"""
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
import numbers
|
|
4
|
+
import os
|
|
4
5
|
import re
|
|
5
6
|
import time
|
|
6
7
|
from datetime import datetime, timedelta
|
|
7
8
|
from typing import TYPE_CHECKING, List, Optional, Union
|
|
8
9
|
|
|
10
|
+
import pandas
|
|
9
11
|
from blazarclient.exception import BlazarClientException
|
|
12
|
+
from ipydatagrid import DataGrid, Expr, TextRenderer
|
|
10
13
|
from IPython.display import display
|
|
11
|
-
from ipywidgets import HTML
|
|
14
|
+
from ipywidgets import HTML, Box, Layout
|
|
12
15
|
from packaging.version import Version
|
|
13
16
|
|
|
14
17
|
from chi import context, server, util
|
|
15
18
|
|
|
16
|
-
from .clients import blazar
|
|
17
|
-
from .context import _is_ipynb
|
|
19
|
+
from .clients import blazar, connection
|
|
20
|
+
from .context import _is_ipynb, get_project_name
|
|
18
21
|
from .exception import CHIValueError, ResourceError, ServiceError
|
|
19
22
|
from .hardware import Device, Node
|
|
20
23
|
from .network import PUBLIC_NETWORK, get_network_id, list_floating_ips
|
|
@@ -259,6 +262,135 @@ class Lease:
|
|
|
259
262
|
|
|
260
263
|
# self.events = lease_json.get('events', [])
|
|
261
264
|
|
|
265
|
+
def _ipython_display_(self):
|
|
266
|
+
"""
|
|
267
|
+
Displays a styled summary of the lease when run in a Jupyter notebook.
|
|
268
|
+
|
|
269
|
+
This method is called automatically by the Jupyter display system when
|
|
270
|
+
an instance of the Lease object is the final expression in a cell.
|
|
271
|
+
It presents key lease attributes using ipywidgets for readability.
|
|
272
|
+
"""
|
|
273
|
+
layout = Layout(padding="4px 10px")
|
|
274
|
+
style = {
|
|
275
|
+
"description_width": "initial",
|
|
276
|
+
"background": "#d3d3d3",
|
|
277
|
+
"white_space": "nowrap",
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
status_style = style.copy()
|
|
281
|
+
status_colors = {
|
|
282
|
+
"ACTIVE": "#a2d9fe",
|
|
283
|
+
"PENDING": "#ffe599",
|
|
284
|
+
"TERMINATED": "#f69084",
|
|
285
|
+
}
|
|
286
|
+
if self.status:
|
|
287
|
+
status_style["background"] = status_colors.get(self.status, "#d3d3d3")
|
|
288
|
+
|
|
289
|
+
children = [
|
|
290
|
+
# HTML(f"<b>Lease ID:</b> {self.id}", style=style, layout=layout),
|
|
291
|
+
HTML(f"<b>Status:</b> {self.status}", style=status_style, layout=layout),
|
|
292
|
+
HTML(f"<b>Name:</b> {self.name}", style=style, layout=layout),
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
if self.start_date:
|
|
296
|
+
children.append(
|
|
297
|
+
HTML(
|
|
298
|
+
f"<b>Start:</b> {self.start_date.strftime('%Y-%m-%d %H:%M')}",
|
|
299
|
+
style=style,
|
|
300
|
+
layout=layout,
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
if self.end_date:
|
|
304
|
+
children.append(
|
|
305
|
+
HTML(
|
|
306
|
+
f"<b>End:</b> {self.end_date.strftime('%Y-%m-%d %H:%M')}",
|
|
307
|
+
style=style,
|
|
308
|
+
layout=layout,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
remaining = None
|
|
313
|
+
if self.end_date and datetime.now() < self.end_date:
|
|
314
|
+
remaining = self.end_date - datetime.now()
|
|
315
|
+
|
|
316
|
+
if remaining:
|
|
317
|
+
days = remaining.days
|
|
318
|
+
hours = remaining.seconds // 3600
|
|
319
|
+
minutes = (remaining.seconds % 3600) // 60
|
|
320
|
+
children.append(
|
|
321
|
+
HTML(
|
|
322
|
+
f"<b>Remaining:</b> {days:02d}d {hours:02d}h {minutes:02d}m",
|
|
323
|
+
style=style,
|
|
324
|
+
layout=layout,
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Reservations
|
|
329
|
+
children.append(
|
|
330
|
+
HTML(
|
|
331
|
+
f"<b>Node Reservations:</b> {len(self.node_reservations)}",
|
|
332
|
+
style=style,
|
|
333
|
+
layout=layout,
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
children.append(
|
|
337
|
+
HTML(
|
|
338
|
+
f"<b>FIP Reservations:</b> {len(self.fip_reservations)}",
|
|
339
|
+
style=style,
|
|
340
|
+
layout=layout,
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
children.append(
|
|
344
|
+
HTML(
|
|
345
|
+
f"<b>Device Reservations:</b> {len(self.device_reservations)}",
|
|
346
|
+
style=style,
|
|
347
|
+
layout=layout,
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if self.project_id:
|
|
352
|
+
try:
|
|
353
|
+
project_name = context.get_project_name(self.project_id)
|
|
354
|
+
children.append(
|
|
355
|
+
HTML(
|
|
356
|
+
f"<b>Project Name:</b> {project_name}",
|
|
357
|
+
style=style,
|
|
358
|
+
layout=layout,
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
except ResourceError:
|
|
362
|
+
children.append(
|
|
363
|
+
HTML(
|
|
364
|
+
f"<b>Project ID:</b> {self.project_id}",
|
|
365
|
+
style=style,
|
|
366
|
+
layout=layout,
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
if self.user_id:
|
|
370
|
+
user_id = connection().get_user_id()
|
|
371
|
+
if self.user_id == user_id:
|
|
372
|
+
label = os.getenv("OS_USERNAME")
|
|
373
|
+
children.append(
|
|
374
|
+
HTML(f"<b>User Name:</b> {label}", style=style, layout=layout)
|
|
375
|
+
)
|
|
376
|
+
else:
|
|
377
|
+
label = self.user_id # [:8] # or just show a truncated ID
|
|
378
|
+
children.append(
|
|
379
|
+
HTML(f"<b>User ID:</b> {label}", style=style, layout=layout)
|
|
380
|
+
)
|
|
381
|
+
if self.created_at:
|
|
382
|
+
children.append(
|
|
383
|
+
HTML(
|
|
384
|
+
f"<b>Created At:</b> {self.created_at.strftime('%Y-%m-%d %H:%M')}",
|
|
385
|
+
style=style,
|
|
386
|
+
layout=layout,
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
box = Box(children=children)
|
|
391
|
+
box.layout = Layout(flex_flow="row wrap")
|
|
392
|
+
display(box)
|
|
393
|
+
|
|
262
394
|
def add_device_reservation(
|
|
263
395
|
self,
|
|
264
396
|
amount: int = None,
|
|
@@ -1079,6 +1211,138 @@ def list_leases() -> List[Lease]:
|
|
|
1079
1211
|
return leases
|
|
1080
1212
|
|
|
1081
1213
|
|
|
1214
|
+
def _status_color(cell):
|
|
1215
|
+
return (
|
|
1216
|
+
"#a2d9fe"
|
|
1217
|
+
if cell.value == "2-ACTIVE"
|
|
1218
|
+
else (
|
|
1219
|
+
"#ffe599"
|
|
1220
|
+
if cell.value == "1-PENDING"
|
|
1221
|
+
else ("#f69084" if cell.value == "3-TERMINATED" else "#e0e0e0")
|
|
1222
|
+
)
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
def show_leases() -> DataGrid:
|
|
1227
|
+
"""
|
|
1228
|
+
Displays a table of the user's leases in an interactive, sortable format.
|
|
1229
|
+
|
|
1230
|
+
Uses an ipydatagrid to present key lease attributes such as ID, name, status,
|
|
1231
|
+
duration, and reservation counts. The grid supports sorting, filtering, and
|
|
1232
|
+
scrolling for easy exploration of lease state.
|
|
1233
|
+
|
|
1234
|
+
Returns:
|
|
1235
|
+
DataGrid: An ipydatagrid widget displaying the leases.
|
|
1236
|
+
"""
|
|
1237
|
+
|
|
1238
|
+
def estimate_column_width(df, column, char_px=7, padding=0):
|
|
1239
|
+
if column not in df.columns:
|
|
1240
|
+
raise ValueError(f"Column '{column}' not found in DataFrame.")
|
|
1241
|
+
max_chars = df[column].astype(str).map(len).max()
|
|
1242
|
+
return max(max_chars * char_px + padding, 80)
|
|
1243
|
+
|
|
1244
|
+
leases = list_leases()
|
|
1245
|
+
|
|
1246
|
+
rows = []
|
|
1247
|
+
for lease in leases:
|
|
1248
|
+
try:
|
|
1249
|
+
project_name = get_project_name(lease.project_id)
|
|
1250
|
+
except ResourceError:
|
|
1251
|
+
project_name = lease.project_id[:8] if lease.project_id else "Unknown"
|
|
1252
|
+
|
|
1253
|
+
if lease.user_id == connection().current_user_id:
|
|
1254
|
+
user_label = os.getenv("OS_USERNAME")
|
|
1255
|
+
else:
|
|
1256
|
+
user_label = lease.user_id if lease.user_id else "Unknown"
|
|
1257
|
+
|
|
1258
|
+
if lease.start_date and lease.end_date:
|
|
1259
|
+
duration_hrs = round(
|
|
1260
|
+
(lease.end_date - lease.start_date).total_seconds() / 3600, 1
|
|
1261
|
+
)
|
|
1262
|
+
else:
|
|
1263
|
+
duration_hrs = "N/A"
|
|
1264
|
+
|
|
1265
|
+
# Inside your row-building loop:
|
|
1266
|
+
if lease.end_date and lease.end_date > datetime.now():
|
|
1267
|
+
remaining_td = lease.end_date - datetime.now()
|
|
1268
|
+
remaining_str = (
|
|
1269
|
+
f"{remaining_td.days:02d}d {(remaining_td.seconds // 3600):02d}h"
|
|
1270
|
+
)
|
|
1271
|
+
elif lease.end_date and lease.end_date <= datetime.now():
|
|
1272
|
+
remaining_str = "Expired"
|
|
1273
|
+
else:
|
|
1274
|
+
remaining_str = "N/A"
|
|
1275
|
+
|
|
1276
|
+
# prepending status with numeric makes it possible to character sort
|
|
1277
|
+
# since ipydatagrid does not allow custom sort functions
|
|
1278
|
+
status_order = {
|
|
1279
|
+
"PENDING": "1-PENDING",
|
|
1280
|
+
"ACTIVE": "2-ACTIVE",
|
|
1281
|
+
"TERMINATED": "3-TERMINATED",
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
rows.append(
|
|
1285
|
+
{
|
|
1286
|
+
"Name": lease.name,
|
|
1287
|
+
"Status": status_order.get(lease.status, f"4-{lease.status}"),
|
|
1288
|
+
"User": user_label,
|
|
1289
|
+
"Project": project_name,
|
|
1290
|
+
"Start": lease.start_date.strftime("%Y-%m-%d %H:%M")
|
|
1291
|
+
if lease.start_date
|
|
1292
|
+
else "",
|
|
1293
|
+
"End": lease.end_date.strftime("%Y-%m-%d %H:%M")
|
|
1294
|
+
if lease.end_date
|
|
1295
|
+
else "",
|
|
1296
|
+
"Remaining": remaining_str,
|
|
1297
|
+
"Total Hours": duration_hrs,
|
|
1298
|
+
"# Nodes": len(lease.node_reservations),
|
|
1299
|
+
"# FIPs": len(lease.fip_reservations),
|
|
1300
|
+
"Created": lease.created_at.strftime("%Y-%m-%d %H:%M")
|
|
1301
|
+
if lease.created_at
|
|
1302
|
+
else "",
|
|
1303
|
+
"Lease ID": lease.id,
|
|
1304
|
+
"_is_user_lease": 0
|
|
1305
|
+
if lease.user_id == connection().current_user_id
|
|
1306
|
+
else 1,
|
|
1307
|
+
}
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1310
|
+
df = pandas.DataFrame(rows)
|
|
1311
|
+
df = pandas.DataFrame(rows)
|
|
1312
|
+
df = df.sort_values(by=["_is_user_lease", "Status", "Created"])
|
|
1313
|
+
df = df.drop(columns=["_is_user_lease"])
|
|
1314
|
+
|
|
1315
|
+
renderers = {
|
|
1316
|
+
"Status": TextRenderer(
|
|
1317
|
+
background_color=Expr(_status_color),
|
|
1318
|
+
text_color="black",
|
|
1319
|
+
),
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
display(
|
|
1323
|
+
DataGrid(
|
|
1324
|
+
df,
|
|
1325
|
+
layout={"height": "400px", "width": "100%"},
|
|
1326
|
+
column_widths={
|
|
1327
|
+
"key": 30,
|
|
1328
|
+
"Name": int(estimate_column_width(df, "Name")),
|
|
1329
|
+
"Status": 120,
|
|
1330
|
+
"Remaining": 80,
|
|
1331
|
+
"Total Hours": 50,
|
|
1332
|
+
"# Nodes": 30,
|
|
1333
|
+
"# FIPs": 30,
|
|
1334
|
+
"Project": 100,
|
|
1335
|
+
"User": 75,
|
|
1336
|
+
"Start": 95,
|
|
1337
|
+
"End": 95,
|
|
1338
|
+
"Created": 95,
|
|
1339
|
+
"Lease ID": 30,
|
|
1340
|
+
},
|
|
1341
|
+
renderers=renderers,
|
|
1342
|
+
)
|
|
1343
|
+
)
|
|
1344
|
+
|
|
1345
|
+
|
|
1082
1346
|
def _get_lease_from_blazar(ref: str):
|
|
1083
1347
|
blazar_client = blazar()
|
|
1084
1348
|
|
|
@@ -3,49 +3,6 @@ from neutronclient.common.exceptions import NotFound
|
|
|
3
3
|
from .clients import neutron
|
|
4
4
|
from .exception import CHIValueError, ResourceError
|
|
5
5
|
|
|
6
|
-
__all__ = [
|
|
7
|
-
"get_network",
|
|
8
|
-
"get_network_id",
|
|
9
|
-
"create_network",
|
|
10
|
-
"delete_network",
|
|
11
|
-
"update_network",
|
|
12
|
-
"list_networks",
|
|
13
|
-
"get_subnet",
|
|
14
|
-
"get_subnet_id",
|
|
15
|
-
"create_subnet",
|
|
16
|
-
"delete_subnet",
|
|
17
|
-
"update_subnet",
|
|
18
|
-
"list_subnets",
|
|
19
|
-
"get_port",
|
|
20
|
-
"get_port_id",
|
|
21
|
-
"create_port",
|
|
22
|
-
"update_port",
|
|
23
|
-
"delete_port",
|
|
24
|
-
"list_ports",
|
|
25
|
-
"get_router",
|
|
26
|
-
"get_router_id",
|
|
27
|
-
"create_router",
|
|
28
|
-
"delete_router",
|
|
29
|
-
"update_router",
|
|
30
|
-
"list_routers",
|
|
31
|
-
"add_route_to_router",
|
|
32
|
-
"add_routes_to_router",
|
|
33
|
-
"remove_route_from_router",
|
|
34
|
-
"remove_routes_from_router",
|
|
35
|
-
"remove_all_routes_from_router",
|
|
36
|
-
"add_port_to_router",
|
|
37
|
-
"add_port_to_router_by_name",
|
|
38
|
-
"add_subnet_to_router",
|
|
39
|
-
"add_subnet_to_router_by_name",
|
|
40
|
-
"remove_subnet_from_router",
|
|
41
|
-
"remove_port_from_router",
|
|
42
|
-
"get_free_floating_ip",
|
|
43
|
-
"get_floating_ip",
|
|
44
|
-
"list_floating_ips",
|
|
45
|
-
"bind_floating_ip",
|
|
46
|
-
"nuke_network",
|
|
47
|
-
]
|
|
48
|
-
|
|
49
6
|
PUBLIC_NETWORK = "public"
|
|
50
7
|
|
|
51
8
|
|
|
@@ -3,17 +3,6 @@ from manilaclient.exceptions import NotFound
|
|
|
3
3
|
from .clients import manila
|
|
4
4
|
from .exception import CHIValueError, ResourceError
|
|
5
5
|
|
|
6
|
-
__all__ = [
|
|
7
|
-
"create_share",
|
|
8
|
-
"delete_share",
|
|
9
|
-
"extend_share",
|
|
10
|
-
"get_access_rules",
|
|
11
|
-
"get_share",
|
|
12
|
-
"get_share_id",
|
|
13
|
-
"list_shares",
|
|
14
|
-
"shrink_share",
|
|
15
|
-
]
|
|
16
|
-
|
|
17
6
|
|
|
18
7
|
def _get_default_share_type_id():
|
|
19
8
|
# we only support one share type - cephfsnfstype
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-chi
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Helper library for Chameleon Infrastructure (CHI) testbed
|
|
5
5
|
Home-page: https://www.chameleoncloud.org
|
|
6
6
|
Author: University of Chicago
|
|
@@ -27,9 +27,11 @@ Requires-Dist: python-novaclient
|
|
|
27
27
|
Requires-Dist: python-swiftclient
|
|
28
28
|
Requires-Dist: python-zunclient
|
|
29
29
|
Requires-Dist: ipython
|
|
30
|
+
Requires-Dist: ipydatagrid
|
|
30
31
|
Requires-Dist: ipywidgets
|
|
31
32
|
Requires-Dist: networkx
|
|
32
33
|
Requires-Dist: matplotlib
|
|
34
|
+
Requires-Dist: pandas
|
|
33
35
|
Dynamic: author
|
|
34
36
|
Dynamic: author-email
|
|
35
37
|
Dynamic: classifier
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"git_version": "3ff8f3d", "is_release": false}
|
python_chi-1.2.2/ChangeLog
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"git_version": "e454aa3", "is_release": false}
|
python_chi-1.2.2/setup.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|