diff --git a/src/globus_cli/commands/streams/__init__.py b/src/globus_cli/commands/streams/__init__.py index 5190c240b..52f57ba1f 100644 --- a/src/globus_cli/commands/streams/__init__.py +++ b/src/globus_cli/commands/streams/__init__.py @@ -6,6 +6,7 @@ lazy_subcommands={ "tunnel": (".tunnel", "tunnel_command"), "access-point": (".access_point", "access_point_command"), + "environment": (".environment", "environment_command"), }, ) def stream_command() -> None: diff --git a/src/globus_cli/commands/streams/environment/__init__.py b/src/globus_cli/commands/streams/environment/__init__.py new file mode 100644 index 000000000..92a5a2620 --- /dev/null +++ b/src/globus_cli/commands/streams/environment/__init__.py @@ -0,0 +1,23 @@ +from globus_cli.parsing import group + + +@group( + "environment", + lazy_subcommands={ + "initialize": (".initialize", "initialize_command"), + "update": (".update", "update_command"), + "cleanup": (".cleanup", "cleanup_command"), + "contact-lookup": (".contact_lookup", "contact_lookup"), + }, +) +def environment_command() -> None: + """Manage Globus Streams application environments. + + These commands are used on the environments where Globus Stream + enabled applications will run. They interact with The Globus Transfer + service and the GCS endpoint associated with this side of the tunnel. + You may be asked to log in twice in a row, once to Transfer and once + to the local GCS service. + + Information about the tunnel is written to files under ~/.globus/tunnels. + """ diff --git a/src/globus_cli/commands/streams/environment/_common.py b/src/globus_cli/commands/streams/environment/_common.py new file mode 100644 index 000000000..a92097f83 --- /dev/null +++ b/src/globus_cli/commands/streams/environment/_common.py @@ -0,0 +1,237 @@ +from __future__ import annotations + +import logging +import os +import random +import shutil +import tempfile +import time +import typing as t +import uuid + +import globus_sdk +import globus_sdk.exc as exc +import globus_sdk.response as response +import globus_sdk.scopes as globus_scopes + +import globus_cli.login_manager as globus_lm +from globus_cli.services.gcs import CustomGCSClient +from globus_cli.services.transfer.client import CustomTransferClient + + +class TunnelConf: + file_key_values = { + "lankey": str, + "lankey_id": str, + "connector_contact_string": str, + "update_time": int, + "connector_contact_string_ttl": int, + "endpoint_id": str, + "fake_port": str, + "tunnel_expiration_time": int, + } + + def __init__( + self, + tunnel_id: uuid.UUID, + basepath: str | globus_sdk.MissingType = globus_sdk.MISSING, + ) -> None: + self.lankey: str | None = None + self.lankey_id: str | None = None + self.connector_contact_string: str | None = None + self.update_time: int | None = None + self.connector_contact_string_ttl: int | None = None + self.endpoint_id: str | None = None + self.fake_port: int | None = None + self.tunnel_expiration_time: int | None = None + + self._normalize_key_file(basepath, tunnel_id) + + for v in self.file_key_values: + setattr(self, v, None) + self.file_existed = False + self._load_values() + if self.fake_port is None: + self.fake_port = random.randint(1000, 5000) + + def _normalize_key_file( + self, basepath: str | globus_sdk.MissingType, tunnel_id: uuid.UUID + ) -> None: + if basepath == globus_sdk.MISSING: + basepath = "~/.globus/tunnels/" + basepath = os.path.expanduser(basepath) + os.makedirs(basepath, exist_ok=True) + os.chmod(basepath, 0o700) + + self.keyfile = os.path.join(basepath, f"{tunnel_id}.conf") + self.key_file_base_dir = basepath + + def _load_values(self) -> None: + if not os.path.exists(self.keyfile): + return + + try: + with open(self.keyfile) as fptr: + for line in fptr.readlines(): + la = line.split("=") + try: + if la[0] in self.file_key_values: + t = self.file_key_values[la[0]] + v: str | None = la[1].strip() + if v: + v = t(v) + else: + v = None + setattr(self, la[0], v) + except IndexError: + logging.info(f"Found an odd line in key file {self.keyfile}") + self.file_existed = True + except Exception as ex: + logging.warn(f"cannot read the file {self.keyfile}: {str(ex)}") + + def update_keyfile(self) -> None: + self.update_time = int(time.time()) + tmp = tempfile.NamedTemporaryFile(delete=False, mode="w") + for k in self.file_key_values: + v = getattr(self, k, None) + if v is None: + v = "" + tmp.write(f"{k}={v}\n") + + tmp.close() + shutil.move(tmp.name, self.keyfile) + os.chmod(self.keyfile, 0o600) + logging.info("A new tunnel file has been created") + + def dumps(self) -> str: + out = "" + for k in self.file_key_values: + v = getattr(self, k, None) + if v is None: + v = "" + out = f"{k}={v}\n{out}" + return out + + +# This class is needed to manage the login requirements for the +# `globus streams environment` commands. These operate differently +# than the other globus commands in that we know they will need to +# log into both GCS and Transfer only, and we know they will be +# running these in an application environment for establishing +# connections through a tunnel +class LoginMgr: + def __init__(self, endpoint_id: uuid.UUID | None = None) -> None: + self.login_manager = globus_lm.LoginManager() + self.endpoint_id = endpoint_id + if endpoint_id is not None: + scope = globus_scopes.GCSEndpointScopes( + str(self.endpoint_id) + ).manage_collections + self.login_manager.add_requirement(str(self.endpoint_id), [scope]) + + if not self.login_manager.is_logged_in(): + self.login_manager.run_login_flow( + no_local_server=True, + epilog="Successful login", + ) + + def get_gcs_client(self) -> CustomGCSClient: + if self.endpoint_id is None: + raise Exception("Not configured for GSC") + return self.login_manager.get_gcs_client(endpoint_id=self.endpoint_id) + + def get_transfer_client(self) -> CustomTransferClient: + return self.login_manager.get_transfer_client() + + +class TransferMgr: + def __init__( + self, tunnel_id: uuid.UUID, transfer_client: CustomTransferClient + ) -> None: + self.tunnel_id = tunnel_id + self.stream_id = None + self.contact_string = None + self.transfer_client = transfer_client + self.tunnel_doc = self.transfer_client.get_tunnel(tunnel_id) + pass + + def update_listener(self, cs: str) -> None: + host, _, port = cs.rpartition(":") + attributes = { + "listener_ip_address": host, + "listener_port": port, + } + data = { + "data": { + "type": "Tunnel", + "attributes": attributes, + } + } + self.transfer_client.update_tunnel(self.tunnel_id, data) + + def get_listener_stream_id(self) -> t.Any: + try: + stream_id = self.tunnel_doc["data"]["relationships"]["listener"]["data"][ + "id" + ] + return stream_id + except KeyError as e: + raise exc.GlobusSDKUsageError( + "The listener access point id is not available" + ) from e + + def get_initiator_stream_id(self) -> t.Any: + try: + stream_id = self.tunnel_doc["data"]["relationships"]["initiator"]["data"][ + "id" + ] + return stream_id + except KeyError as e: + raise exc.GlobusSDKUsageError( + "The initiator access point id is not available" + ) from e + + def get_contact_string(self) -> t.Any: + try: + host = self.tunnel_doc["data"]["attributes"]["initiator_ip_address"] + port = self.tunnel_doc["data"]["attributes"]["initiator_port"] + return f"{host}:{port}" + except KeyError as e: + raise exc.GlobusSDKUsageError( + "The initiator contact string is not available" + ) from e + + def get_tunnel_lifetime(self) -> t.Any: + try: + lifetime_mins = self.tunnel_doc["data"]["attributes"]["lifetime_mins"] + return lifetime_mins + except KeyError as e: + raise exc.GlobusSDKUsageError("The tunnel lifetime is not available") from e + + def get_tunnel_doc(self) -> response.GlobusHTTPResponse: + return self.tunnel_doc + + +class GCSMgr: + def __init__( + self, stream_id: uuid.UUID, tunnel_id: uuid.UUID, gcs_client: CustomGCSClient + ) -> None: + self.gcs_client = gcs_client + self.stream_id = stream_id + self.tunnel_id = tunnel_id + + def gcs_get_lankey(self, stream_id: uuid.UUID) -> tuple[str, str]: + data = { + "DATA_TYPE": "lan_secret_create#1.0.0", + "tunnel_id": self.tunnel_id, + "stream_access_point_id": stream_id, + } + res = self.gcs_client.post("/lan_secrets", data=data) + secret = res["data"][0]["secret"] + lankey_id = res["data"][0]["id"] + return secret, lankey_id + + def gcs_delete_lankey(self, lankey_id: uuid.UUID) -> None: + res = self.gcs_client.delete(f"/lan_secrets/{lankey_id}") + if res["http_response_code"] != 200: + raise Exception(res["message"]) diff --git a/src/globus_cli/commands/streams/environment/cleanup.py b/src/globus_cli/commands/streams/environment/cleanup.py new file mode 100644 index 000000000..4c1c14b87 --- /dev/null +++ b/src/globus_cli/commands/streams/environment/cleanup.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import glob +import os +import sys +import time +import uuid + +import click + +from ._common import TunnelConf + + +def clean_one(tunnel_id: uuid.UUID, force: bool, base_dir: str, tm_now: int) -> bool: + conf_obj = TunnelConf(tunnel_id, base_dir) + if not conf_obj.file_existed: + click.echo(f"The tunnel {tunnel_id} does not exist") + return False + + if ( + conf_obj.tunnel_expiration_time is None + or conf_obj.tunnel_expiration_time < tm_now + or force + ): + # we should probably have a prompt + click.echo(f"Deleting the tunnel configuration {tunnel_id}") + os.remove(conf_obj.keyfile) + return True + else: + click.echo(f"The tunnel {tunnel_id} is not expired") + return False + + +@click.command(name="cleanup") +@click.argument("tunnel_id", type=click.UUID, required=False) +@click.option( + "--force", + help="Force a delete even if the tunnel is not expired", + type=bool, + is_flag=True, +) +@click.option( + "--all", + help="Look at every tunnel configuration file and delete them if expired", + type=bool, + is_flag=True, +) +@click.option( + "--base-dir", + help=( + "The directory that stores configuration information " + "(default: ~/.globus/tunnels)" + ), + type=str, + default="~/.globus/tunnels", +) +def cleanup_command( + tunnel_id: uuid.UUID | None, force: bool, all: bool, base_dir: str +) -> None: + """Cleanup Tunnel configuration files. + + This command will simply delete the information created on a local system + with the initialize command. + """ + tm_now = int(time.time()) + if all and force: + if not click.confirm( + "This will delete all your tunnel configurations even if they are not " + "expired, are you sure?" + ): + return + + base_dir = os.path.expanduser(base_dir) + if not all: + if tunnel_id is None: + click.echo("You must provide a tunnel id if not using --all") + sys.exit(1) + deleted = clean_one(tunnel_id, force, base_dir, tm_now) + if not deleted: + sys.exit(2) + else: + pattern = os.path.join(base_dir, "*.conf") + for filepath in glob.glob(pattern): + filepath = os.path.basename(filepath) + tid = filepath[:-5] + clean_one(uuid.UUID(tid), force, base_dir, tm_now) diff --git a/src/globus_cli/commands/streams/environment/contact_lookup.py b/src/globus_cli/commands/streams/environment/contact_lookup.py new file mode 100644 index 000000000..af5e21956 --- /dev/null +++ b/src/globus_cli/commands/streams/environment/contact_lookup.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import time +import uuid + +import click +import globus_sdk + +from globus_cli.parsing import OMITTABLE_STRING + +from . import _common as gt_utils + + +@click.command(name="contact-lookup") +@click.argument("tunnel_id", type=click.UUID) +@click.option( + "--base-dir", + help=( + "The directory that stores configuration information " + "(default: ~/.globus/tunnels)" + ), + type=OMITTABLE_STRING, + default=globus_sdk.MISSING, +) +@click.option( + "--skip-update", + help=( + "Do not attempt to fetch a new contact string from Globus Transfer. In this " + "case an expired cached contact string could be returned." + ), + type=bool, + is_flag=True, +) +def contact_lookup( + tunnel_id: uuid.UUID, base_dir: str | globus_sdk.MissingType, skip_update: bool +) -> None: + """Lookup a Globus Tunnel ID in the local environment. + + This command is largely used by the Globus Tunnel helper libraries + and applications, however it can be run directly. If the contact string + associated with the Tunnel has expired this tool will form a connection + zwith Globus Transfer in order to fetch a new one. + """ + conf_obj = gt_utils.TunnelConf(tunnel_id, base_dir) + if not conf_obj.file_existed: + raise Exception( + f"The environment is not configured for the Globus Tunnel {tunnel_id}" + ) + + tm_now = int(time.time()) + + if not skip_update and ( + conf_obj.connector_contact_string is None + or ( + conf_obj.update_time is not None + and conf_obj.connector_contact_string_ttl is not None + and tm_now > conf_obj.update_time + conf_obj.connector_contact_string_ttl + ) + ): + login_mgr = gt_utils.LoginMgr() + xfer_client = login_mgr.get_transfer_client() + xfer_mgr = gt_utils.TransferMgr(tunnel_id, xfer_client) + conf_obj.connector_contact_string = xfer_mgr.get_contact_string() + conf_obj.update_keyfile() + + print(conf_obj.dumps()) diff --git a/src/globus_cli/commands/streams/environment/initialize.py b/src/globus_cli/commands/streams/environment/initialize.py new file mode 100644 index 000000000..1e2dec6ff --- /dev/null +++ b/src/globus_cli/commands/streams/environment/initialize.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import time +import uuid + +import click +import globus_sdk +import globus_sdk.services.gcs.errors as gcserrors + +from globus_cli.parsing import OMITTABLE_STRING, command + +from ._common import GCSMgr, LoginMgr, TransferMgr, TunnelConf + + +@command("initialize", short_help="Initialize a Globus Streams environment.") +@click.argument("tunnel_id", metavar="TUNNEL_ID", type=click.UUID) +@click.argument("endpoint_id", type=click.UUID, required=False) +@click.option( + "--cs-ttl", + help=( + "If the contact string was last updated more than the value of this ttl, " + "fetch it again. In seconds" + ), + type=int, + default=10, +) +@click.option( + "--base-dir", + help=( + "The directory that stores configuration information " + "(default: ~/.globus/tunnels)" + ), + type=OMITTABLE_STRING, + default=globus_sdk.MISSING, +) +@click.option( + "--listener-contact-string", + help="When initializing an application listener provide its contact string", + type=OMITTABLE_STRING, + default=globus_sdk.MISSING, +) +@click.option( + "--listener", + help=( + "This is the listener side. This is implied when " + "--listener-contact-string is used." + ), + is_flag=True, +) +@click.option( + "--force", + help="Force an update of the LAN secret even if it exists", + is_flag=True, +) +def initialize_command( + tunnel_id: uuid.UUID, + endpoint_id: uuid.UUID | None, + listener_contact_string: str | globus_sdk.MissingType, + cs_ttl: int, + base_dir: str | globus_sdk.MissingType, + listener: bool, + force: bool, +) -> None: + """ + Set up a local environment for use with a Globus Streams. + + User applications can connect to each other via a Globus Tunnel. One side of + the application is the listener and the other side is the connector. There + are helper libraries and tools that can be used by each side of the application + to seamlessly form connections to each other through the Globus Tunnel. In order + to get all the needed security and configuration information to these helper + libraries this command is used. It must be run on both the connector side + and the again on the listener side with the --listener-contact-string option. + """ + click.echo(f"Initializing the environment for tunnel: {tunnel_id}") + + login_mgr = LoginMgr(endpoint_id=endpoint_id) + xfer_client = login_mgr.get_transfer_client() + xfer_mgr = TransferMgr(tunnel_id, xfer_client) + + conf_obj = TunnelConf(tunnel_id, base_dir) + + if listener_contact_string != globus_sdk.MISSING or listener: + listener = True + if listener_contact_string != globus_sdk.MISSING: + xfer_mgr.update_listener(listener_contact_string) + stream_ap_id = xfer_mgr.get_listener_stream_id() + else: + contact_string = xfer_mgr.get_contact_string() + if contact_string: + conf_obj.connector_contact_string = contact_string + conf_obj.connector_contact_string_ttl = cs_ttl + stream_ap_id = xfer_mgr.get_initiator_stream_id() + + if endpoint_id is None: + r = xfer_client.get_stream_access_point(stream_ap_id) + endpoint_id = r["data"]["relationships"]["host_endpoint"]["data"]["id"] + login_mgr = LoginMgr(endpoint_id=endpoint_id) + + gcs_mgr = GCSMgr(stream_ap_id, tunnel_id, login_mgr.get_gcs_client()) + if conf_obj.file_existed: + if force: + if not conf_obj.lankey_id: + click.secho( + "WARNING: There is no lankey id in the configuration file " + "so we cannot delete the old key", + fg="yellow", + ) + else: + gcs_mgr.gcs_delete_lankey(uuid.UUID(conf_obj.lankey_id)) + else: + raise Exception("Configuration exists. Use --force to overwrite it.") + + try: + lankey, lankey_id = gcs_mgr.gcs_get_lankey(stream_ap_id) + conf_obj.lankey = lankey + conf_obj.lankey_id = lankey_id + except gcserrors.GCSAPIError: + click.secho("The LAN connection for this tunnel is not secured", fg="yellow") + pass + + tm_now = int(time.time()) + conf_obj.tunnel_expiration_time = tm_now + xfer_mgr.get_tunnel_lifetime() + conf_obj.endpoint_id = str(endpoint_id) + conf_obj.update_keyfile() + + click.echo(f"The environment is initialized for use with tunnel {tunnel_id}") + click.echo( + f"Your application key file base directory is {conf_obj.key_file_base_dir}" + ) + + if not listener: + click.echo(f"Your contact string is: globus.{tunnel_id}:{conf_obj.fake_port}") diff --git a/src/globus_cli/commands/streams/environment/update.py b/src/globus_cli/commands/streams/environment/update.py new file mode 100644 index 000000000..c7c93a4e3 --- /dev/null +++ b/src/globus_cli/commands/streams/environment/update.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import uuid + +import click +import globus_sdk + +from globus_cli.parsing import OMITTABLE_INT, OMITTABLE_STRING + +from . import _common as gt_utils + + +@click.command(name="update") +@click.argument("tunnel_id", type=click.UUID) +@click.option( + "--cs-ttl", + help=( + "If the contact string was last updated more than the value of this ttl," + "fetch it again" + ), + type=OMITTABLE_INT, + default=globus_sdk.MISSING, +) +@click.option( + "--base-dir", + help=( + "The directory that stores configuration information " + "(default: ~/.globus/tunnels)" + ), + type=OMITTABLE_STRING, + default=globus_sdk.MISSING, +) +@click.option( + "--listener-contact-string", + help="When initializing an application listener provide its contact string", + type=OMITTABLE_STRING, + default=globus_sdk.MISSING, +) +def update_command( + tunnel_id: uuid.UUID, + listener_contact_string: str | globus_sdk.MissingType, + cs_ttl: int | globus_sdk.MissingType, + base_dir: str | globus_sdk.MissingType, +) -> None: + """Update the Globus Tunnel information in the local environment. + + This command is used to update the login tokens, contact string, + or application listener string in the local environment. It does + not update the lan secret. + """ + + click.echo(f"Updating the environment for tunnel: {tunnel_id}") + + conf_obj = gt_utils.TunnelConf(tunnel_id, base_dir) + if not conf_obj.file_existed: + raise FileNotFoundError( + f"The file {conf_obj.keyfile} was not found. Initialize the " + "environment first." + ) + + login_mgr = gt_utils.LoginMgr() + xfer_client = login_mgr.get_transfer_client() + xfer_mgr = gt_utils.TransferMgr(tunnel_id, xfer_client) + + if listener_contact_string != globus_sdk.MISSING: + click.echo( + "Updating the application listener on the Tunnel: " + f"{tunnel_id} to: {listener_contact_string}" + ) + xfer_mgr.update_listener(listener_contact_string) + else: + cs = xfer_mgr.get_contact_string() + if cs: + conf_obj.connector_contact_string = cs + if cs_ttl != globus_sdk.MISSING: + conf_obj.connector_contact_string_ttl = cs_ttl + conf_obj.update_keyfile() diff --git a/src/globus_cli/commands/streams/tunnel/create.py b/src/globus_cli/commands/streams/tunnel/create.py index 6665cb1f8..dacebd3dc 100644 --- a/src/globus_cli/commands/streams/tunnel/create.py +++ b/src/globus_cli/commands/streams/tunnel/create.py @@ -57,6 +57,7 @@ def create_tunnel_command( lifetime_mins=lifetime_minutes, restartable=restartable, ) + tunnel_client = login_manager.get_transfer_client() res = tunnel_client.create_tunnel(data) display( diff --git a/tests/files/api_fixtures/streams_environment.yaml b/tests/files/api_fixtures/streams_environment.yaml new file mode 100644 index 000000000..f6ef31131 --- /dev/null +++ b/tests/files/api_fixtures/streams_environment.yaml @@ -0,0 +1,108 @@ +metadata: + tunnel_id: "689dcb43-cdbe-46bb-9a59-dad71091a247" + lankey_id: "13fb1936-c8dd-4c6b-9af3-394075c08424" + lankey: "93333879ea95" + endpoint_id: "a98f60bd-98bf-47bd-9a1d-6a0a3b70eef9" + + +gcs: + - path: /api/lan_secrets + method: POST + json: + { + "DATA": [ + { + "secret": "93333879ea95", + "id": "13fb1936-c8dd-4c6b-9af3-394075c08424" + } + ] + } + + - path: /api/lan_secrets + method: DELETE + +transfer: + - path: /v0.10/endpoint/a98f60bd-98bf-47bd-9a1d-6a0a3b70eef9 + method: GET + json: + { + "DATA": [ + { + "DATA_TYPE": "server", + "hostname": "abc.xyz.data.globus.org" + } + ], + "DATA_TYPE": "endpoint", + "activated": false, + "canonical_name": "a98f60bd-98bf-47bd-9a1d-6a0a3b70eef9#myserver", + "contact_email": null, + "contact_info": null, + "default_directory": null, + "description": "example gcsv5 endpoint streaming", + "department": null, + "display_name": "myserver", + "entity_type": "GCSv5_endpoint", + "force_encryption": false, + "gcs_manager_url": "https://abc.xyz.data.globus.org", + "gcs_version": "5.4.10", + "high_assurance": false, + "host_endpoint_id": null, + "id": "a98f60bd-98bf-47bd-9a1d-6a0a3b70eef9", + "is_globus_connect": false, + "info_link": null, + "keywords": null, + "local_user_info_available": false, + "non_functional": true, + "organization": "My Org", + "owner_id": "a98f60bd-98bf-47bd-9a1d-6a0a3b70eef9", + "owner_string": "a98f60bd-98bf-47bd-9a1d-6a0a3b70eef9@clients.auth.globus.org", + "public": false, + "shareable": false, + "subscription_id": null + } + + - path: /v2/tunnels/689dcb43-cdbe-46bb-9a59-dad71091a247 + method: GET + json: + { + "data": { + "attributes": { + "created_time": "2025-12-08T22:00:05.239465", + "initiator_ip_address": null, + "initiator_port": null, + "label": "Test Stream Access Point", + "lifetime_mins": 360, + "listener_ip_address": null, + "listener_port": null, + "restartable": false, + "state": "AWAITING_LISTENER", + "status": "The tunnel is waiting for listening contact detail setup.", + "submission_id": "3fc91c9a-d481-11f0-ae61-0affc202d2e9" + }, + "id": "689dcb43-cdbe-46bb-9a59-dad71091a247", + "relationships": { + "initiator": { + "data": { + "id": "80583f05-75f3-4825-b8a5-6c3edf0bbc5c", + "type": "StreamAccessPoint" + } + }, + "listener": { + "data": { + "id": "dd5fa993-749f-48fb-86cf-f07ad5797d7e", + "type": "StreamAccessPoint" + } + }, + "owner": { + "data": { + "id": "4d443580-012d-4954-816f-e0592bd356e1", + "type": "Identity" + } + } + }, + "type": "Tunnel" + }, + "meta": { + "request_id": "yDZD2RwPk" + } + } diff --git a/tests/functional/streams/test_environment.py b/tests/functional/streams/test_environment.py new file mode 100644 index 000000000..86f10f8cb --- /dev/null +++ b/tests/functional/streams/test_environment.py @@ -0,0 +1,65 @@ +import glob +import os +import tempfile +import time +import unittest.mock as mock +import uuid + +from globus_sdk.testing import load_response_set + +import globus_cli.commands.streams.environment._common as gtutils +import globus_cli.login_manager + +_g_xfer_mgr = "gtutils.TransferMgr" +_g_login_mgr = "gtutils.LoginMgr" +_g_gcs_mgr = "gtutils.GCSMgr" +# +# +# @pytest.mark.parametrize("output_format", ["json", "text"]) +# def test_help(run_line, output_format): +# result = run_line(["globus", "streams", "environment", "--help"]) +# assert result.exit_code == 0 + + +def test_initialize_happy(run_line, add_gcs_login): + meta = load_response_set("cli.streams_environment").metadata + + lankey = meta["lankey"] + lankey_id = meta["lankey_id"] + tunnel_id = meta["tunnel_id"] + cs = "localhost:9999" + cs_ttl = 99 + endpoint_id = meta["endpoint_id"] + + add_gcs_login(endpoint_id) + + with ( + tempfile.TemporaryDirectory() as tmp_path, + mock.patch( + "globus_cli.login_manager.LoginManager.is_logged_in", return_value=True + ), + ): + + # lm.is_logged_in.return_value = True + result = run_line( + [ + "globus", + "streams", + "environment", + "initialize", + "--base-dir", + tmp_path, + "--cs-ttl", + cs_ttl, + str(tunnel_id), + endpoint_id, + ], + ) + assert result.exit_code == 0 + + conf = gtutils.TunnelConf(tunnel_id, tmp_path) + assert conf.endpoint_id == endpoint_id + assert conf.connector_contact_string == cs + assert conf.connector_contact_string_ttl == cs_ttl + assert conf.lankey == lankey + assert conf.lankey_id == lankey_id diff --git a/tests/unit/streams/test_stream_config.py b/tests/unit/streams/test_stream_config.py new file mode 100644 index 000000000..8098627c4 --- /dev/null +++ b/tests/unit/streams/test_stream_config.py @@ -0,0 +1,38 @@ +import os +import tempfile +import uuid + +import globus_cli.commands.streams.environment._common as gtutils + + +def test_default_values_config(): + with tempfile.TemporaryDirectory() as tmp_path: + tunnel_id = uuid.uuid4() + conf = gtutils.TunnelConf(tunnel_id, tmp_path) + assert not conf.file_existed + + for f in conf.file_key_values: + if f != "fake_port": + assert getattr(conf, f) is None + + +def test_set_values_config(): + lankey = "XXXXX" + lankey_id = str(uuid.uuid4()) + with tempfile.TemporaryDirectory() as tmp_path: + tunnel_id = uuid.uuid4() + conf = gtutils.TunnelConf(tunnel_id, tmp_path) + + conf.lankey = lankey + conf.lankey_id = lankey_id + + conf.update_keyfile() + + assert os.path.exists(conf.keyfile) + + conf2 = gtutils.TunnelConf(tunnel_id, tmp_path) + + assert conf2.file_existed + assert conf2.lankey == lankey + assert conf2.lankey_id == lankey_id + assert conf2.connector_contact_string is None