Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Soufianej hardware 1.0 #60

Merged
merged 15 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
2 changes: 2 additions & 0 deletions chi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@
"nova",
"zun",
]

context.use_site(context.DEFAULT_SITE)
263 changes: 229 additions & 34 deletions chi/context.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
from itertools import chain
import os
import sys
import time
import openstack

from typing import List, Optional
from keystoneauth1.identity.v3 import OidcAccessToken
from keystoneauth1 import loading
from keystoneauth1.loading.conf import _AUTH_SECTION_OPT, _AUTH_TYPE_OPT
from keystoneauth1 import session
from keystoneclient.v3.client import Client as KeystoneClient
from oslo_config import cfg
from IPython.display import display

import ipywidgets as widgets
import requests

from . import jupyterhub
from .exception import CHIValueError, ResourceError

import openstack
import ipywidgets as widgets
import requests
import logging

LOG = logging.getLogger(__name__)

DEFAULT_SITE = "CHI@UC"
DEFAULT_IMAGE_NAME = "CC-Ubuntu22.04"
DEFAULT_NODE_TYPE = "compute_skylake"
DEFAULT_AUTH_TYPE = "v3token"
CONF_GROUP = "chi"
RESOURCE_API_URL = os.getenv("CHI_RESOURCE_API_URL", "https://api.chameleoncloud.org")


def default_key_name():
username = os.getenv("USER")
return f"{username}-jupyter" if username else None
Expand Down Expand Up @@ -170,6 +181,24 @@ def _check_deprecated(key):
)
return deprecated_extra_opts[key]

def _is_ipynb() -> bool:
try:
from IPython import get_ipython
if 'IPKernelApp' not in get_ipython().config:
return False
except ImportError:
return False
return True

def _is_ipynb() -> bool:
try:
from IPython import get_ipython
if 'IPKernelApp' not in get_ipython().config:
return False
except ImportError:
return False
return True


def set(key, value):
"""Set a context parameter by name.
Expand Down Expand Up @@ -225,7 +254,6 @@ def get(key):
else:
return cfg.CONF[_auth_section(_auth_plugin)][key]


def params():
"""List all parameters currently set on the context.

Expand All @@ -238,7 +266,80 @@ def params():
return keys


def use_site(site_name):
def list_sites(show: Optional[str] = None) -> List[str]:
"""
Retrieves a list of Chameleon sites.

Args:
show (str, optional): Determines how the site names should be displayed.
Possible values are "widget" to display as a table widget, "text" to print
as plain text,or None (default) to return the List[str] of site names.

Returns:
If `show` is set to "widget", it displays the site names as a text widget.
If `show` is set to "text", it prints the site names as plain text.
If `show` is set to None, it returns a list of site names.

Raises:
ValueError: If no sites are returned or if an invalid value is provided for the `show` parameter.
"""
global _sites

if not _sites:
res = requests.get(f"{RESOURCE_API_URL}/sites.json")
res.raise_for_status()
items = res.json().get("items", [])
_sites = {s["name"]: s for s in items}
_sites = dict(sorted(_sites.items(),
key=lambda x: (x[1]['site_class'], x[0] not in ["CHI@TACC", "CHI@UC"])))
_sites["KVM@TACC"] = {
"name": "KVM@TACC",
"web": "https://kvm.tacc.chameleoncloud.org",
"location": "Austin, Texas, USA",
"user_support_contact": "[email protected]",
}
if not _sites:
raise ResourceError("No sites returned.")

if show == None:
return _sites
elif show == "widget" and _is_ipynb():
# Constructing the table HTML
table_html = """
<table>
<tr>
<th>Name</th>
<th>URL</th>
<th>Location</th>
<th>User Support Contact</th>
</tr>
"""

for site_name in _sites.keys():
table_html += f"""
<tr>
<td>{site_name}</td>
<td>{_sites[site_name]["web"]}</td>
<td>{_sites[site_name]["location"]}</td>
<td>{_sites[site_name]["user_support_contact"]}</td>
</tr>
"""

table_html += "</table>"
display(widgets.HTML(value=table_html))
elif show == "text":
print("Chameleon Sites:")
for site_name in _sites.keys():
site = _sites[site_name]
print(f"- Name: {site_name}")
print(f" URL: {site['web']}")
print(f" Location: {site['location']}")
print(f" User Support Contact: {site['user_support_contact']}")
else:
raise CHIValueError("Invalid value for 'show' parameter.")


def use_site(site_name: str) -> None:
"""Configure the global request context to target a particular CHI site.

Targeting a site will mean that leases, instance launch requests, and any
Expand All @@ -264,13 +365,8 @@ def use_site(site_name):
"""
global _sites
if not _sites:
res = requests.get(f"{RESOURCE_API_URL}/sites.json")
try:
res.raise_for_status()
items = res.json().get("items", [])
_sites = {s["name"]: s for s in items}
if not _sites:
raise ValueError("No sites returned.")
_sites = list_sites()
except Exception:
printerr(
"""Failed to fetch list of available Chameleon sites.
Expand All @@ -285,31 +381,13 @@ def use_site(site_name):

site = _sites.get(site_name)
if not site:
# TODO(jason): Remove this fallback when CHI@Edge is enrolled into
# the resource discovery API and the resource catalogue has support for it.
if site_name == "CHI@Edge":
site = {
"name": "CHI@Edge",
"web": "https://chi.edge.chameleoncloud.org",
"location": "Distributed",
"user_support_contact": "https://groups.google.com/g/chameleon-edge-users",
}
elif site_name == "KVM@TACC":
site = {
"name": "KVM@TACC",
"web": "https://kvm.tacc.chameleoncloud.org",
"location": "Austin, Texas, USA",
"user_support_contact": "[email protected]",
}
else:
raise ValueError(
(
f'No site named "{site_name}" exists! Possible values: '
", ".join(_sites.keys())
)
raise CHIValueError(
(
f'No site named "{site_name}" exists! Possible values: '
", ".join(_sites.keys())
)
)

# Set important parameters
set("auth_url", f'{site["web"]}:5000/v3')
set("region_name", site["name"])

Expand All @@ -321,6 +399,124 @@ def use_site(site_name):
]
print("\n".join(output))

def choose_site() -> None:
"""
Displays a dropdown menu to select a chameleon site.

Only works if running in a Ipynb notebook environment.
"""
if _is_ipynb():
global _sites
if not _sites:
_sites = list_sites()
use_site(list(_sites.keys())[0])
print("Please choose a site in the dropdown below")
site_dropdown = widgets.Dropdown(options=_sites.keys(), description="Select Site")
display(site_dropdown)
site_dropdown.observe(lambda change: use_site(change['new']), names='value')
else:
print("Choose site feature is only available in an ipynb environment.")


def list_projects(show: str = None) -> List[str]:
"""
Retrieves a list of projects associated with the current user.

Args:
show (str, optional): Determines how the project names should be displayed.
Possible values are "widget" to display as a table widget, "text" to print
as plain text, or None (default) to return the list of project names.

Returns:
If `show` is set to "widget", it displays the project names as a text widget.
If `show` is set to "text", it prints the project names as plain text.
If `show` is set to None, it returns a list of project names.

Raises:
ValueError: If no projects are returned or an invalid value is provided for the `show` parameter.

"""
keystone_session = session()
keystone_client = KeystoneClient(
session=keystone_session,
interface=getattr(keystone_session, "interface", None),
region_name=getattr(keystone_session, "region_name", None),
)

projects = keystone_client.projects.list(user=keystone_session.get_user_id())
project_names = [project.name for project in projects]

if show == "widget":
table_html = "<table>"
for project in project_names:
table_html += f"<tr><td>{project}</td></tr>"
table_html += "</table>"

display(widgets.HTML(table_html))
elif show == "text":
print("\n".join(project_names))
elif show == None:
return list(project_names)
else:
raise CHIValueError("Invalid value for 'show' parameter.")

def use_project(project: str) -> None:
"""
Sets the current project name.

Args:
project (str): The name of the project to use.

Returns:
None
"""
set('project_name', project)
print(f"Now using project: {project}")

def choose_project() -> None:
"""
Displays a dropdown menu to select a project.

Only works if running in a Ipynb notebook environment.
"""
if _is_ipynb():
project_dropdown = widgets.Dropdown(options=list_projects(), description="Select Project")
display(project_dropdown)
use_project(list_projects()[0])
project_dropdown.observe(lambda change: (use_project(change['new'])), names='value')
else:
print("Choose project feature is only available in Jupyter notebook environment.")

def check_credentials() -> None:
"""
Prints authentication metadata (e.g. username, site) and if credentials are currently valid and user is authenticated.
"""
try:
print(f"Username: {os.getenv('USER')}")
print(f"Currently site: {get('region_name')}" )
print(f"Currently project: {get('project_name')}" )
print("Projects:")
for project in list_projects():
print(project)
print("Authentication is valid.")
except Exception as e:
print("Authentication failed: ", str(e))

def set_log_level(level: str = "ERROR") -> None:
"""Configures logger for python-chi. By default, only errors are shown.
Set to "DEBUG" to see debug level logging, which will show calls to external APIs.

Args:
level (str, optional): The log level. Defaults to "ERROR".
"""
if level == "DEBUG":
openstack.enable_logging(debug=True, http_debug=True)
LOG.setLevel(logging.DEBUG)
elif level == "ERROR":
openstack.enable_logging(debug=False, http_debug=False)
LOG.setLevel(logging.ERROR)
else:
raise CHIValueError("Invalid log level value, please choose between 'ERROR' and 'DEBUG'")

def session():
"""Get a Keystone Session object suitable for authenticating a client.
Expand All @@ -337,7 +533,6 @@ def session():
)
return _session


def reset():
"""Reset the context, removing all overrides and defaults.

Expand Down
27 changes: 27 additions & 0 deletions chi/exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class CHIValueError(Exception):
"""Raised when argument is not valid. These errors might be fixed by
checking hardware catalog or documentation. Examples where this might
be seen are:
- Site name is not valid
- Node type is not valid
- Resource does not exist
"""
def __init__(self, message):
super().__init__(message)


class ResourceError(Exception):
"""Raised when a request has valid arguments, but the resources are
being used incorrectly, or can not be used as requested. This type
of error might depend on the time the request is run, due to the
shared nature of the testbed."""
def __init__(self, message):
super().__init__(message)


class ServiceError(Exception):
"""Raised when an error occurs with some Chameleon resource.
For example, if your node is having hardware issues, and so
fails to provision, this will be raised."""
def __init__(self, message):
super().__init__(message)
Loading
Loading