From 695bd02037a7a8abd6b0ac76ae1ac1559ae46c41 Mon Sep 17 00:00:00 2001 From: Vivian Thiebaut <81188381+vthiebaut10@users.noreply.github.com> Date: Fri, 28 Jun 2024 00:17:21 -0400 Subject: [PATCH] [SSH]Install Arc SSH Proxy from MAR instead of Storage Blob (#7726) --- src/ssh/HISTORY.md | 4 + src/ssh/azext_ssh/connectivity_utils.py | 305 +++++++++++------- src/ssh/azext_ssh/constants.py | 5 +- src/ssh/azext_ssh/custom.py | 2 +- .../tests/latest/test_connectivity_utils.py | 140 ++++++++ src/ssh/azext_ssh/tests/latest/test_custom.py | 4 +- src/ssh/setup.py | 5 +- 7 files changed, 334 insertions(+), 131 deletions(-) create mode 100644 src/ssh/azext_ssh/tests/latest/test_connectivity_utils.py diff --git a/src/ssh/HISTORY.md b/src/ssh/HISTORY.md index 429430032d3..22788f0f9ea 100644 --- a/src/ssh/HISTORY.md +++ b/src/ssh/HISTORY.md @@ -1,5 +1,9 @@ Release History =============== +2.0.4 +----- +* Install Arc SSH Proxy from MAR + 2.0.3 ----- * [Bug Fix] Ensure that certificate validity value is always an integer when retrieving relay information for connecting to Arc Machines. diff --git a/src/ssh/azext_ssh/connectivity_utils.py b/src/ssh/azext_ssh/connectivity_utils.py index bb43e588282..cd7596d2da3 100644 --- a/src/ssh/azext_ssh/connectivity_utils.py +++ b/src/ssh/azext_ssh/connectivity_utils.py @@ -4,11 +4,11 @@ # -------------------------------------------------------------------------------------------- import time -import stat import os -import urllib.request import json import base64 +import oras.client +import tarfile from glob import glob import colorama @@ -185,160 +185,219 @@ def _list_credentials(cmd, resource_uri, certificate_validity_in_seconds): return ListCredential(cli_ctx=cmd.cli_ctx)(command_args=list_cred_args) +def format_relay_info_string(relay_info): + relay_info_string = json.dumps( + { + "relay": { + "namespaceName": relay_info['namespaceName'], + "namespaceNameSuffix": relay_info['namespaceNameSuffix'], + "hybridConnectionName": relay_info['hybridConnectionName'], + "accessKey": relay_info['accessKey'], + "expiresOn": relay_info['expiresOn'], + "serviceConfigurationToken": relay_info['serviceConfigurationToken'] + } + }) + result_bytes = relay_info_string.encode("ascii") + enc = base64.b64encode(result_bytes) + base64_result_string = enc.decode("ascii") + return base64_result_string + + +def _handle_relay_connection_delay(cmd, message): + # relay has retry delay after relay connection is lost + # must sleep for at least as long as the delay + # otherwise the ssh connection will fail + progress_bar = cmd.cli_ctx.get_progress_controller(True) + for x in range(0, consts.SERVICE_CONNECTION_DELAY_IN_SECONDS + 1): + interval = float(1 / consts.SERVICE_CONNECTION_DELAY_IN_SECONDS) + progress_bar.add(message=f"{message}:", + value=interval * x, total_val=1.0) + time.sleep(1) + progress_bar.add(message=f"{message}: complete", + value=1.0, total_val=1.0) + progress_bar.end() + + # Downloads client side proxy to connect to Arc Connectivity Platform -def get_client_side_proxy(arc_proxy_folder): +def install_client_side_proxy(arc_proxy_folder): - request_uri, install_location, older_version_location = _get_proxy_filename_and_url(arc_proxy_folder) - install_dir = os.path.dirname(install_location) + client_operating_system = _get_client_operating_system() + client_architecture = _get_client_architeture() + install_dir = _get_proxy_install_dir(arc_proxy_folder) + proxy_name = _get_proxy_filename(client_operating_system, client_architecture) + install_location = os.path.join(install_dir, proxy_name) # Only download new proxy if it doesn't exist already if not os.path.isfile(install_location): - t0 = time.time() - # download the executable - try: - with urllib.request.urlopen(request_uri) as response: - response_content = response.read() - response.close() - except Exception as e: - raise azclierror.ClientRequestError(f"Failed to download client proxy executable from {request_uri}. " - "Error: " + str(e)) from e - time_elapsed = time.time() - t0 - - proxy_data = { - 'Context.Default.AzureCLI.SSHProxyDownloadTime': time_elapsed, - 'Context.Default.AzureCLI.SSHProxyVersion': consts.CLIENT_PROXY_VERSION - } - telemetry.add_extension_event('ssh', proxy_data) - - # if directory doesn't exist, create it if not os.path.isdir(install_dir): - file_utils.create_directory(install_dir, f"Failed to create client proxy directory '{install_dir}'. ") + file_utils.create_directory(install_dir, f"Failed to create client proxy directory '{install_dir}'.") # if directory exists, delete any older versions of the proxy else: + older_version_location = _get_older_version_proxy_path( + install_dir, + client_operating_system, + client_architecture) older_version_files = glob(older_version_location) for f in older_version_files: file_utils.delete_file(f, f"failed to delete older version file {f}", warning=True) - # write executable in the install location - file_utils.write_to_file(install_location, 'wb', response_content, "Failed to create client proxy file. ") - os.chmod(install_location, os.stat(install_location).st_mode | stat.S_IXUSR) - print_styled_text((Style.SUCCESS, f"SSH Client Proxy saved to {install_location}")) - - _download_proxy_license(arc_proxy_folder) + _download_proxy_from_MCR(install_dir, proxy_name, client_operating_system, client_architecture) + _check_proxy_installation(install_dir, proxy_name) return install_location -def _get_proxy_filename_and_url(arc_proxy_folder): - import platform - operating_system = platform.system() - machine = platform.machine() +def _download_proxy_from_MCR(dest_dir, proxy_name, operating_system, architecture): + mar_target = f"{consts.CLIENT_PROXY_MCR_TARGET}/{operating_system.lower()}/{architecture}/ssh-proxy" + logger.debug("Downloading Arc Connectivity Proxy from %s in Microsoft Artifact Regristy.", mar_target) - logger.debug("Platform OS: %s", operating_system) - logger.debug("Platform architecture: %s", machine) + client = oras.client.OrasClient() + t0 = time.time() - if "arm64" in machine.lower() or "aarch64" in machine.lower(): - architecture = 'arm64' - elif machine.endswith('64'): - architecture = 'amd64' - elif machine.endswith('86'): - architecture = '386' - elif machine == '': - raise azclierror.BadRequestError("Couldn't identify the platform architecture.") - else: - raise azclierror.BadRequestError(f"Unsuported architecture: {machine} is not currently supported") + try: + response = client.pull(target=f"{mar_target}:{consts.CLIENT_PROXY_VERSION}", outdir=dest_dir) + except Exception as e: + raise azclierror.CLIInternalError( + f"Failed to download Arc Connectivity proxy with error {str(e)}. Please try again.") - # define the request url and install location based on the os and architecture. - proxy_name = f"sshProxy_{operating_system.lower()}_{architecture}" - request_uri = (f"{consts.CLIENT_PROXY_STORAGE_URL}/{consts.CLIENT_PROXY_RELEASE}" - f"/{proxy_name}_{consts.CLIENT_PROXY_VERSION}") - install_location = proxy_name + "_" + consts.CLIENT_PROXY_VERSION.replace('.', '_') - older_location = proxy_name + "*" - - if operating_system == 'Windows': - request_uri = request_uri + ".exe" - install_location = install_location + ".exe" - older_location = older_location + ".exe" - elif operating_system not in ('Linux', 'Darwin'): - raise azclierror.BadRequestError(f"Unsuported OS: {operating_system} platform is not currently supported") + time_elapsed = time.time() - t0 - if not arc_proxy_folder: - install_location = os.path.expanduser(os.path.join('~', os.path.join(".clientsshproxy", install_location))) - older_location = os.path.expanduser(os.path.join('~', os.path.join(".clientsshproxy", older_location))) - else: - install_location = os.path.join(arc_proxy_folder, install_location) - older_location = os.path.join(arc_proxy_folder, older_location) + proxy_data = { + 'Context.Default.AzureCLI.SSHProxyDownloadTime': time_elapsed, + 'Context.Default.AzureCLI.SSHProxyVersion': consts.CLIENT_PROXY_VERSION + } + telemetry.add_extension_event('ssh', proxy_data) - return request_uri, install_location, older_location + proxy_package_path = _get_proxy_package_path_from_oras_response(response) + _extract_proxy_tar_files(proxy_package_path, dest_dir, proxy_name) + file_utils.delete_file(proxy_package_path, f"Failed to delete {proxy_package_path}. Please delete manually.", True) -def _download_proxy_license(proxy_dir): - if not proxy_dir: - proxy_dir = os.path.join('~', ".clientsshproxy") - license_uri = f"{consts.CLIENT_PROXY_STORAGE_URL}/{consts.CLIENT_PROXY_RELEASE}/LICENSE.txt" - license_install_location = os.path.expanduser(os.path.join(proxy_dir, "LICENSE.txt")) +def _get_proxy_package_path_from_oras_response(pull_response): + if not isinstance(pull_response, list): + raise azclierror.CLIInternalError( + "Attempt to download Arc Connectivity Proxy returned unnexpected result. Please try again.") - notice_uri = f"{consts.CLIENT_PROXY_STORAGE_URL}/{consts.CLIENT_PROXY_RELEASE}/ThirdPartyNotice.txt" - notice_install_location = os.path.expanduser(os.path.join(proxy_dir, "ThirdPartyNotice.txt")) + if len(pull_response) != 1: + for r in pull_response: + file_utils.delete_file(r, f"Failed to delete {r}. Please delete it manually.", True) + raise azclierror.CLIInternalError( + "Attempt to download Arc Connectivity Proxy returned unnexpected result. Please try again.") - _get_and_write_proxy_license_files(license_uri, license_install_location, "License") - _get_and_write_proxy_license_files(notice_uri, notice_install_location, "Third Party Notice") + proxy_package_path = pull_response[0] + if not os.path.isfile(proxy_package_path): + raise azclierror.CLIInternalError("Unable to download Arc Connectivity Proxy. Please try again.") -def _get_and_write_proxy_license_files(uri, install_location, target_name): - try: - license_content = _download_from_uri(uri) - file_utils.write_to_file(file_path=install_location, - mode='wb', - content=license_content, - error_message=f"Failed to create {target_name} file at {install_location}.") - # pylint: disable=broad-except - except Exception: - logger.warning("Failed to download Connection Proxy %s file from %s.", target_name, uri) + logger.debug("Proxy package downloaded to %s", proxy_package_path) - print_styled_text((Style.SUCCESS, f"SSH Connection Proxy {target_name} saved to {install_location}.")) + return proxy_package_path -def _download_from_uri(request_uri): - response_content = None - with urllib.request.urlopen(request_uri) as response: - response_content = response.read() - response.close() +def _extract_proxy_tar_files(proxy_package_path, install_dir, proxy_name): + with tarfile.open(proxy_package_path, 'r:gz') as tar: + members = [] + for member in tar.getmembers(): + if member.isfile(): + filenames = member.name.split('/') - if response_content is None: - raise azclierror.ClientRequestError(f"Failed to download file from {request_uri}") + if len(filenames) != 2: + tar.close() + file_utils.delete_file( + proxy_package_path, + f"Failed to delete {proxy_package_path}. Please delete it manually.", + True) + raise azclierror.CLIInternalError( + "Attempt to download Arc Connectivity Proxy returned unnexpected result. Please try again.") - return response_content + member.name = filenames[1] + if member.name.startswith('sshproxy'): + member.name = proxy_name + elif member.name.lower() not in ['license.txt', 'thirdpartynotice.txt']: + tar.close() + file_utils.delete_file( + proxy_package_path, + f"Failed to delete {proxy_package_path}. Please delete it manually.", + True) + raise azclierror.CLIInternalError( + "Attempt to download Arc Connectivity Proxy returned unnexpected result. Please try again.") -def format_relay_info_string(relay_info): - relay_info_string = json.dumps( - { - "relay": { - "namespaceName": relay_info['namespaceName'], - "namespaceNameSuffix": relay_info['namespaceNameSuffix'], - "hybridConnectionName": relay_info['hybridConnectionName'], - "accessKey": relay_info['accessKey'], - "expiresOn": relay_info['expiresOn'], - "serviceConfigurationToken": relay_info['serviceConfigurationToken'] - } - }) - result_bytes = relay_info_string.encode("ascii") - enc = base64.b64encode(result_bytes) - base64_result_string = enc.decode("ascii") - return base64_result_string + members.append(member) + tar.extractall(members=members, path=install_dir) -def _handle_relay_connection_delay(cmd, message): - # relay has retry delay after relay connection is lost - # must sleep for at least as long as the delay - # otherwise the ssh connection will fail - progress_bar = cmd.cli_ctx.get_progress_controller(True) - for x in range(0, consts.SERVICE_CONNECTION_DELAY_IN_SECONDS + 1): - interval = float(1 / consts.SERVICE_CONNECTION_DELAY_IN_SECONDS) - progress_bar.add(message=f"{message}:", - value=interval * x, total_val=1.0) - time.sleep(1) - progress_bar.add(message=f"{message}: complete", - value=1.0, total_val=1.0) - progress_bar.end() + +def _check_proxy_installation(install_dir, proxy_name): + proxy_filepath = os.path.join(install_dir, proxy_name) + if os.path.isfile(proxy_filepath): + print_styled_text((Style.SUCCESS, f"Successfuly installed SSH Connectivity Proxy file {proxy_filepath}")) + else: + raise azclierror.CLIInternalError( + "Failed to install required SSH Arc Connectivity Proxy. " + f"Couldn't find expected file {proxy_filepath}. Please try again.") + + license_files = ["LICENSE.txt", "ThirdPartyNotice.txt"] + for file in license_files: + file_location = os.path.join(install_dir, file) + if os.path.isfile(file_location): + print_styled_text( + (Style.SUCCESS, + f"Successfuly installed SSH Connectivity Proxy License file {file_location}")) + else: + logger.warning( + "Failed to download Arc Connectivity Proxy license file %s. Clouldn't find expected file %s. " + "This won't affect your connection.", file, file_location) + + +def _get_proxy_filename(operating_system, architecture): + if operating_system.lower() == 'darwin' and architecture == '386': + raise azclierror.BadRequestError("Unsupported Darwin OS with 386 architecture.") + proxy_filename = \ + f"sshProxy_{operating_system.lower()}_{architecture}_{consts.CLIENT_PROXY_VERSION.replace('.', '_')}" + if operating_system.lower() == 'windows': + proxy_filename += '.exe' + return proxy_filename + + +def _get_older_version_proxy_path(install_dir, operating_system, architecture): + proxy_name = f"sshProxy_{operating_system.lower()}_{architecture}_*" + return os.path.join(install_dir, proxy_name) + + +def _get_proxy_install_dir(arc_proxy_folder): + if not arc_proxy_folder: + return os.path.expanduser(os.path.join('~', ".clientsshproxy")) + return arc_proxy_folder + + +def _get_client_architeture(): + import platform + machine = platform.machine() + architecture = None + + logger.debug("Platform architecture: %s", machine) + + if "arm64" in machine.lower() or "aarch64" in machine.lower(): + architecture = 'arm64' + elif machine.endswith('64'): + architecture = 'amd64' + elif machine.endswith('86'): + architecture = '386' + elif machine == '': + raise azclierror.BadRequestError("Couldn't identify the platform architecture.") + else: + raise azclierror.BadRequestError(f"Unsuported architecture: {machine} is not currently supported") + + return architecture + + +def _get_client_operating_system(): + import platform + operating_system = platform.system() + + logger.debug("Platform OS: %s", operating_system) + + if operating_system.lower() not in ('linux', 'darwin', 'windows'): + raise azclierror.BadRequestError(f"Unsuported OS: {operating_system} platform is not currently supported") + return operating_system diff --git a/src/ssh/azext_ssh/constants.py b/src/ssh/azext_ssh/constants.py index 6333076e126..b75c616cf7c 100644 --- a/src/ssh/azext_ssh/constants.py +++ b/src/ssh/azext_ssh/constants.py @@ -7,9 +7,8 @@ AGENT_MINIMUM_VERSION_MAJOR = 1 AGENT_MINIMUM_VERSION_MINOR = 31 -CLIENT_PROXY_VERSION = "1.3.026031" -CLIENT_PROXY_RELEASE = "release17-02-24" -CLIENT_PROXY_STORAGE_URL = "https://sshproxysa.blob.core.windows.net" +CLIENT_PROXY_VERSION = "1.3.026973" +CLIENT_PROXY_MCR_TARGET = "mcr.microsoft.com/azureconnectivity/proxy" CLEANUP_TOTAL_TIME_LIMIT_IN_SECONDS = 120 CLEANUP_TIME_INTERVAL_IN_SECONDS = 10 CLEANUP_AWAIT_TERMINATION_IN_SECONDS = 30 diff --git a/src/ssh/azext_ssh/custom.py b/src/ssh/azext_ssh/custom.py index f13cd706596..13baf9e3388 100644 --- a/src/ssh/azext_ssh/custom.py +++ b/src/ssh/azext_ssh/custom.py @@ -187,7 +187,7 @@ def _do_ssh_op(cmd, op_info, op_call): try: if op_info.is_arc(): - op_info.proxy_path = connectivity_utils.get_client_side_proxy(op_info.ssh_proxy_folder) + op_info.proxy_path = connectivity_utils.install_client_side_proxy(op_info.ssh_proxy_folder) (op_info.relay_info, op_info.new_service_config) = connectivity_utils.get_relay_information( cmd, op_info.resource_group_name, op_info.vm_name, op_info.resource_type, cert_lifetime, op_info.port, op_info.yes_without_prompt) diff --git a/src/ssh/azext_ssh/tests/latest/test_connectivity_utils.py b/src/ssh/azext_ssh/tests/latest/test_connectivity_utils.py new file mode 100644 index 00000000000..07f8fced41b --- /dev/null +++ b/src/ssh/azext_ssh/tests/latest/test_connectivity_utils.py @@ -0,0 +1,140 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import unittest +from unittest import mock +from azext_ssh import connectivity_utils + +from azure.cli.core import azclierror + + +class SshConnectivityUtilsCommandTest(unittest.TestCase): + + @mock.patch('platform.machine') + def test_get_client_architecture_arm64(self, mock_plat): + mock_plat.return_value = 'arm64' + arch = connectivity_utils._get_client_architeture() + self.assertEqual(arch, 'arm64') + + @mock.patch('platform.machine') + def test_get_client_architecture_aarch64(self, mock_plat): + mock_plat.return_value = 'aarch64' + arch = connectivity_utils._get_client_architeture() + self.assertEqual(arch, 'arm64') + + @mock.patch('platform.machine') + def test_get_client_architecture_amd64(self, mock_plat): + mock_plat.return_value = 'AMD64' + arch = connectivity_utils._get_client_architeture() + self.assertEqual(arch, 'amd64') + + @mock.patch('platform.machine') + def test_get_client_architecture_x86(self, mock_plat): + mock_plat.return_value = 'x64_86' + arch = connectivity_utils._get_client_architeture() + self.assertEqual(arch, '386') + + @mock.patch('platform.machine') + def test_get_client_architecture_empty(self, mock_plat): + mock_plat.return_value = '' + with self.assertRaises(azclierror.BadRequestError): + arch = connectivity_utils._get_client_architeture() + + @mock.patch('platform.machine') + def test_get_client_architecture_unsupported(self, mock_plat): + mock_plat.return_value = 'blabla' + with self.assertRaises(azclierror.BadRequestError): + arch = connectivity_utils._get_client_architeture() + + @mock.patch('platform.system') + def test_get_client_os_unsupported(self, mock_plat): + mock_plat.return_value = 'blabla' + with self.assertRaises(azclierror.BadRequestError): + arch = connectivity_utils._get_client_operating_system() + + def test_get_proxy_filename_amd_windows(self): + name = connectivity_utils._get_proxy_filename('Windows', 'amd64') + self.assertEqual(name, 'sshProxy_windows_amd64_1_3_026973.exe') + + def test_get_proxy_filename_arm_linux(self): + name = connectivity_utils._get_proxy_filename('Linux', 'arm64') + self.assertEqual(name, 'sshProxy_linux_arm64_1_3_026973') + + def test_get_proxy_filename_arm_Darwin(self): + name = connectivity_utils._get_proxy_filename('Darwin', 'arm64') + self.assertEqual(name, 'sshProxy_darwin_arm64_1_3_026973') + + def test_get_proxy_filename_386_linuux(self): + name = connectivity_utils._get_proxy_filename('Linux', '386') + self.assertEqual(name, 'sshProxy_linux_386_1_3_026973') + + def test_get_proxy_filename_386_darwin(self): + with self.assertRaises(azclierror.BadRequestError): + name = connectivity_utils._get_proxy_filename('Darwin', '386') + + @mock.patch('os.path.isfile') + def test_check_proxy_is_installed_fail(self, mock_isfile): + mock_isfile.side_effect = [False, True, True] + with self.assertRaises(azclierror.CLIInternalError): + connectivity_utils._check_proxy_installation("/dir/", "proxy") + + @mock.patch('os.path.isfile') + def test_check_proxy_is_installed_sucess(self, mock_isfile): + mock_isfile.side_effect = [True, True, True] + connectivity_utils._check_proxy_installation("/dir/", "proxy") + + @mock.patch('os.path.isfile') + def test_check_proxy_is_installed_fail_licenses(self, mock_isfile): + mock_isfile.side_effect = [True, False, False] + connectivity_utils._check_proxy_installation("/dir/", "proxy") + + @mock.patch('tarfile.open') + def test_extract_proxy_from_tar(self, mock_open): + mock_tar = mock_open.return_value.__enter__.return_value + + mock_file1 = mock.Mock() + mock_file1.name = "dir/sshproxy" + mock_file1.isfile = mock.Mock(return_value=True) + + mock_file2 = mock.Mock() + mock_file2.name = "dir/license.txt" + mock_file2.isfile = mock.Mock(return_value=True) + + mock_file3 = mock.Mock() + mock_file3.name = "dir/thirdpartynotice.txt" + mock_file3.isfile = mock.Mock(return_value=True) + + mock_file4 = mock.Mock() + mock_file4.name = "dir" + mock_file4.isfile = mock.Mock(return_value=False) + + mock_tar.getmembers.return_value = [mock_file1, mock_file2, mock_file3, mock_file4] + + connectivity_utils._extract_proxy_tar_files("proxy_package.tar.gz", "/tmp/install", "my_proxy") + + mock_tar.extractall.assert_called_once_with(members=[mock_file1, mock_file2, mock_file3], path="/tmp/install") + + self.assertEquals(mock_file1.name, "my_proxy") + self.assertEquals(mock_file2.name, "license.txt") + self.assertEquals(mock_file3.name, "thirdpartynotice.txt") + + @mock.patch('os.path.isfile') + @mock.patch('platform.machine') + @mock.patch('platform.system') + @mock.patch('azext_ssh.connectivity_utils._get_proxy_install_dir') + @mock.patch('os.path.join') + @mock.patch('azext_ssh.file_utils.create_directory') + @mock.patch('azext_ssh.connectivity_utils._download_proxy_from_MCR') + @mock.patch('azext_ssh.connectivity_utils._check_proxy_installation') + def test_install_proxy_create_dir(self, mock_check, mock_download, mock_dir, mock_join, mock_get_proxy_dir, mock_sys, mock_machine, mock_isfile): + mock_machine.return_value = 'aarch64' + mock_sys.return_value = 'linux' + mock_get_proxy_dir.return_value = "/dir/proxy" + mock_isfile.return_value = False + + connectivity_utils.install_client_side_proxy(None) + + mock_dir.assert_called_once_with("/dir/proxy", "Failed to create client proxy directory \'/dir/proxy\'.") + mock_download.assert_called_once_with("/dir/proxy", "sshProxy_linux_arm64_1_3_026973", "linux", "arm64") + mock_check.assert_called_once_with("/dir/proxy", "sshProxy_linux_arm64_1_3_026973") diff --git a/src/ssh/azext_ssh/tests/latest/test_custom.py b/src/ssh/azext_ssh/tests/latest/test_custom.py index b8130983058..66800a8537e 100644 --- a/src/ssh/azext_ssh/tests/latest/test_custom.py +++ b/src/ssh/azext_ssh/tests/latest/test_custom.py @@ -414,7 +414,7 @@ def test_do_ssh_op_no_public_ip(self, mock_ip, mock_check_files): mock_ip.assert_called_once_with(cmd, "rg", "vm", False) mock_op.assert_not_called() - @mock.patch('azext_ssh.connectivity_utils.get_client_side_proxy') + @mock.patch('azext_ssh.connectivity_utils.install_client_side_proxy') @mock.patch('azext_ssh.connectivity_utils.get_relay_information') @mock.patch('azext_ssh.ssh_utils.start_ssh_connection') @mock.patch('azext_ssh.custom._check_or_create_public_private_files') @@ -438,7 +438,7 @@ def test_do_ssh_op_arc_local_user(self, mock_get_cert, mock_check_keys, mock_sta mock_get_cert.assert_not_called() mock_check_keys.assert_not_called() - @mock.patch('azext_ssh.connectivity_utils.get_client_side_proxy') + @mock.patch('azext_ssh.connectivity_utils.install_client_side_proxy') @mock.patch('azext_ssh.custom.connectivity_utils.get_relay_information') @mock.patch('azext_ssh.ssh_utils.get_ssh_cert_principals') @mock.patch('os.path.join') diff --git a/src/ssh/setup.py b/src/ssh/setup.py index f664949d4e0..2403edc17e3 100644 --- a/src/ssh/setup.py +++ b/src/ssh/setup.py @@ -7,7 +7,7 @@ from setuptools import setup, find_packages -VERSION = "2.0.3" +VERSION = "2.0.4" CLASSIFIERS = [ 'Development Status :: 4 - Beta', @@ -22,7 +22,8 @@ ] DEPENDENCIES = [ - 'oschmod==0.3.12' + 'oschmod==0.3.12', + 'oras==0.1.30' ] setup(