From 55508c10e051965a54747aa687d29725ea8fb02b Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Mon, 15 Jun 2026 13:44:35 -0500 Subject: [PATCH 1/5] Add support for device auth flow --- chi/context.py | 71 +++++++++++++++++++++++++++++++++++++++++++----- requirements.txt | 2 +- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/chi/context.py b/chi/context.py index 150d6fb..df917ff 100644 --- a/chi/context.py +++ b/chi/context.py @@ -8,6 +8,7 @@ import ipywidgets as widgets import openstack import requests +from ccauth.plugin import ChameleonDeviceAuth from IPython.display import display from keystoneauth1 import loading, session from keystoneauth1.identity.v3 import OidcAccessToken @@ -31,6 +32,14 @@ EDGE_RESOURCE_API_URL = os.getenv( "EDGE_RESOURCE_API_URL", "https://chameleoncloud.org/edge-hw-discovery/devices" ) +DEFAULT_CLIENT_ID = "chi-cli-device-token" +DEFAULT_DISCOVERY_ENDPOINT = ( + "https://auth.chameleoncloud.org/auth/realms/chameleon" + "/.well-known/openid-configuration" +) +DEFAULT_PROTOCOL = "openid" +DEFAULT_RESOURCE_PROVIDER = "chameleon" +DEFAULT_PROJECT_DOMAIN_NAME = "chameleon" def default_key_name(): @@ -74,6 +83,7 @@ def default_key_name(): _session = None _sites = {} _lease_id = None +_device_auth = False version = "1.1" @@ -366,7 +376,7 @@ def use_site(site_name: str) -> None: Args: site_name (str): The name of the site, e.g., "CHI@UC". """ - global _sites + global _sites, _session if not _sites: try: _sites = list_sites() @@ -392,6 +402,11 @@ def use_site(site_name: str) -> None: ) ) + _session = None + + set( + "project_domain_name", DEFAULT_PROJECT_DOMAIN_NAME + ) # Same for all chameleon sites set("auth_url", f"{site['web']}:5000/v3") set("region_name", site["name"]) @@ -404,6 +419,25 @@ def use_site(site_name: str) -> None: print("\n".join(output)) +def use_device_auth(enable: bool = True) -> None: + """Enable or disable device authorization for subsequent sessions. + + Call `use_device_auth()` before creating a session (or before `use_site`) + to opt into the device authorization flow. Pass `False` to disable. + + Args: + enable (bool): True to enable device auth, False to disable. + """ + global _device_auth, _session + _device_auth = bool(enable) + + _session = None + if _device_auth: + print("Device authorization enabled.") + else: + print("Device authorization disabled.") + + def choose_site(default: str = None) -> None: """ Displays a dropdown menu to select a chameleon site. @@ -650,13 +684,34 @@ def session(): Returns: keystoneauth1.session.Session: the authentication session object. """ - global _session + global _session, _device_auth if not _session: - auth = loading.load_auth_from_conf_options(cfg.CONF, CONF_GROUP) - sess = SessionLoader().load_from_conf_options(cfg.CONF, CONF_GROUP, auth=auth) - _session = loading.load_adapter_from_conf_options( - cfg.CONF, CONF_GROUP, session=sess - ) + if _device_auth: + auth_url = get("auth_url") + plugin = ChameleonDeviceAuth( + auth_url=auth_url, + identity_provider=DEFAULT_RESOURCE_PROVIDER, + protocol=DEFAULT_PROTOCOL, + client_id=DEFAULT_CLIENT_ID, + discovery_endpoint=DEFAULT_DISCOVERY_ENDPOINT, + scope="openid", + project_name=get("project_name"), + project_domain_name=get("project_domain_name"), + ) + sess = SessionLoader().load_from_conf_options( + cfg.CONF, CONF_GROUP, auth=plugin + ) + _session = loading.load_adapter_from_conf_options( + cfg.CONF, CONF_GROUP, session=sess + ) + else: + auth = loading.load_auth_from_conf_options(cfg.CONF, CONF_GROUP) + sess = SessionLoader().load_from_conf_options( + cfg.CONF, CONF_GROUP, auth=auth + ) + _session = loading.load_adapter_from_conf_options( + cfg.CONF, CONF_GROUP, session=sess + ) return _session @@ -671,8 +726,10 @@ def reset(): """ global _session global _sites + global _device_auth _session = None _sites = {} + _device_auth = False cfg.CONF.reset() _set_auth_plugin( os.getenv("OS_AUTH_TYPE", os.getenv("OS_AUTH_METHOD", DEFAULT_AUTH_TYPE)) diff --git a/requirements.txt b/requirements.txt index 9d2a10b..487ac19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,4 +17,4 @@ ipywidgets networkx matplotlib pandas - +git+https://github.com/ChameleonCloud/ccauth.git From c2251ba62dbc212311b9d8a6e9d0a439c619dc6c Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Mon, 15 Jun 2026 14:17:42 -0500 Subject: [PATCH 2/5] Bump GHA python --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2921933..be82c42 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,11 +10,11 @@ on: jobs: test: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: python: - - 3.8 + - 3.10 steps: - uses: actions/checkout@v4 - name: Set up Python 3.x From 906db2819f2c3a35e67eb0b0ed020d365d70ef76 Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Mon, 15 Jun 2026 14:22:54 -0500 Subject: [PATCH 3/5] Fix GHA version parsing --- .github/workflows/test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be82c42..cd2436f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,10 @@ jobs: strategy: matrix: python: - - 3.10 + - "3.10" + - "3.11" + - "3.12" + - "3.13" steps: - uses: actions/checkout@v4 - name: Set up Python 3.x From 79f5174dc9177a65fdb9d1d881b548826aa80598 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 17 Nov 2025 17:26:45 -0600 Subject: [PATCH 4/5] Replace python-zunclient with GitHub URL --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 487ac19..6ab65f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ python-manilaclient python-neutronclient python-novaclient python-swiftclient -python-zunclient +git+https://github.com/chameleoncloud/python-zunclient ipython ipydatagrid ipywidgets From a1074e996c1698f1dd16d8a8f517b62f593616e0 Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Fri, 26 Jun 2026 11:43:00 -0500 Subject: [PATCH 5/5] Update device auth support --- chi/context.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/chi/context.py b/chi/context.py index df917ff..215332b 100644 --- a/chi/context.py +++ b/chi/context.py @@ -8,7 +8,6 @@ import ipywidgets as widgets import openstack import requests -from ccauth.plugin import ChameleonDeviceAuth from IPython.display import display from keystoneauth1 import loading, session from keystoneauth1.identity.v3 import OidcAccessToken @@ -38,7 +37,8 @@ "/.well-known/openid-configuration" ) DEFAULT_PROTOCOL = "openid" -DEFAULT_RESOURCE_PROVIDER = "chameleon" +DEFAULT_IDENTITY_PROVIDER = "chameleon" +DEFAULT_SCOPE = "openid" DEFAULT_PROJECT_DOMAIN_NAME = "chameleon" @@ -404,9 +404,7 @@ def use_site(site_name: str) -> None: _session = None - set( - "project_domain_name", DEFAULT_PROJECT_DOMAIN_NAME - ) # Same for all chameleon sites + set("project_domain_name", DEFAULT_PROJECT_DOMAIN_NAME) set("auth_url", f"{site['web']}:5000/v3") set("region_name", site["name"]) @@ -688,13 +686,21 @@ def session(): if not _session: if _device_auth: auth_url = get("auth_url") + try: + from ccauth.plugin import ChameleonDeviceAuth + except ImportError as e: + raise CHIValueError( + "Device auth requested but package 'ccauth' is not installed." + " Install 'ccauth' to use device authorization." + ) from e + plugin = ChameleonDeviceAuth( auth_url=auth_url, - identity_provider=DEFAULT_RESOURCE_PROVIDER, + identity_provider=DEFAULT_IDENTITY_PROVIDER, protocol=DEFAULT_PROTOCOL, client_id=DEFAULT_CLIENT_ID, discovery_endpoint=DEFAULT_DISCOVERY_ENDPOINT, - scope="openid", + scope=DEFAULT_SCOPE, project_name=get("project_name"), project_domain_name=get("project_domain_name"), )