Skip to content

Commit

Permalink
Merge pull request #17 from vthiebaut10/ssharc-v3
Browse files Browse the repository at this point in the history
SSHArc 3
  • Loading branch information
vthiebaut10 authored Dec 20, 2021
2 parents 63c89db + b01b728 commit b6e4cb6
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 198 deletions.
17 changes: 17 additions & 0 deletions src/ssh/HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
Release History
===============
0.2.2
-----
* Validate that target machine exists before attempting to connect.
* ssh config accepts relative path for --file.
* Make --local-user mandatory for Windows target machines.
* For ssh config, relay information is stored under az_ssh_config folder.
* New optional parameter --arc-proxy-folder to determine where arc proxy is stored.
* Relay information lifetime is synced with certificate lifetime for AAD login.

0.2.1
-----
* SSHArc Private Preview 2

0.2.0
-----
* SSHArc Private Preview 1

0.1.9
-----
* Add support for connecting to Arc Servers using AAD issued certificates.
Expand Down
9 changes: 9 additions & 0 deletions src/ssh/azext_ssh/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ def load_arguments(self, _):
help=('This is an internal argument. This argument is used by Azure Portal to provide a one click '
'SSH login experience in Cloud shell.'),
deprecate_info=c.deprecate(hide=True), action='store_true')
c.argument('ssh_proxy_folder', options_list=['--ssh-proxy-folder'],
help=('Path to the folder where the ssh proxy should be saved. '
'Default to .clientsshproxy folder in user\'s home directory if not provided.'))
c.positional('ssh_args', nargs='*', help='Additional arguments passed to OpenSSH')

with self.argument_context('ssh config') as c:
Expand All @@ -47,6 +50,9 @@ def load_arguments(self, _):
c.argument('resource_type', options_list=['--resource-type'],
help='Resource type should be either Microsoft.Compute or Microsoft.HybridCompute')
c.argument('cert_file', options_list=['--certificate-file', '-c'], help='Path to certificate file')
c.argument('ssh_proxy_folder', options_list=['--ssh-proxy-folder'],
help=('Path to the folder where the ssh proxy should be saved. '
'Default to .clientsshproxy folder in user\'s home directory if not provided.'))

with self.argument_context('ssh cert') as c:
c.argument('cert_path', options_list=['--file', '-f'],
Expand All @@ -69,4 +75,7 @@ def load_arguments(self, _):
help=('This is an internal argument. This argument is used by Azure Portal to provide a one click '
'SSH login experience in Cloud shell.'),
deprecate_info=c.deprecate(hide=True), action='store_true')
c.argument('ssh_proxy_folder', options_list=['--ssh-proxy-folder'],
help=('Path to the folder where the ssh proxy should be saved. '
'Default to .clientsshproxy folder in user\'s home directory if not provided.'))
c.positional('ssh_args', nargs='*', help='Additional arguments passed to OpenSSH')
152 changes: 152 additions & 0 deletions src/ssh/azext_ssh/connectivity_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import time
import stat
import os
import urllib.request
import json
import base64
from glob import glob

from azure.cli.core import telemetry
from azure.cli.core import azclierror
from knack import log

from . import file_utils
from . import constants as consts

logger = log.get_logger(__name__)


# Get the Access Details to connect to Arc Connectivity platform from the HybridConnectivity RP
def get_relay_information(cmd, resource_group, vm_name, certificate_validity_in_seconds):
from azext_ssh._client_factory import cf_endpoint
client = cf_endpoint(cmd.cli_ctx)

if not certificate_validity_in_seconds or \
certificate_validity_in_seconds > consts.RELAY_INFO_MAXIMUM_DURATION_IN_SECONDS:
certificate_validity_in_seconds = consts.RELAY_INFO_MAXIMUM_DURATION_IN_SECONDS

try:
t0 = time.time()
result = client.list_credentials(resource_group_name=resource_group, machine_name=vm_name,
endpoint_name="default", expiresin=certificate_validity_in_seconds)
time_elapsed = time.time() - t0
telemetry.add_extension_event('ssh', {'Context.Default.AzureCLI.SSHListCredentialsTime': time_elapsed})
except Exception as e:
telemetry.set_exception(exception='Call to listCredentials failed',
fault_type=consts.LIST_CREDENTIALS_FAILED_FAULT_TYPE,
summary=f'listCredentials failed with error: {str(e)}.')
raise azclierror.ClientRequestError(f"Request for Azure Relay Information Failed: {str(e)}")

return result


# Downloads client side proxy to connect to Arc Connectivity Platform
def get_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)

# 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:
telemetry.set_exception(exception=e, fault_type=consts.PROXY_DOWNLOAD_FAILED_FAULT_TYPE,
summary=f'Failed to download proxy from {request_uri}')
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}'. ")
# if directory exists, delete any older versions of the proxy
else:
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)

return install_location


def _get_proxy_filename_and_url(arc_proxy_folder):
import platform
operating_system = platform.system()
machine = platform.machine()

logger.debug("Platform OS: %s", operating_system)
logger.debug("Platform architecture: %s", machine)

if machine.endswith('64'):
architecture = 'amd64'
elif machine.endswith('86'):
architecture = '386'
elif machine == '':
raise azclierror.BadRequestError("Couldn't identify the platform architecture.")
else:
telemetry.set_exception(exception='Unsuported architecture for installing proxy',
fault_type=consts.PROXY_UNSUPPORTED_ARCH_FAULT_TYPE,
summary=f'{machine} is not supported for installing client proxy')
raise azclierror.BadRequestError(f"Unsuported architecture: {machine} is not currently supported")

# 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'):
telemetry.set_exception(exception='Unsuported OS for installing ssh client proxy',
fault_type=consts.PROXY_UNSUPPORTED_OS_FAULT_TYPE,
summary=f'{operating_system} is not supported for installing client proxy')
raise azclierror.BadRequestError(f"Unsuported OS: {operating_system} platform is not currently supported")

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)

return request_uri, install_location, older_location


def format_relay_info_string(relay_info):
relay_info_string = json.dumps(
{
"relay": {
"namespaceName": relay_info.namespace_name,
"namespaceNameSuffix": relay_info.namespace_name_suffix,
"hybridConnectionName": relay_info.hybrid_connection_name,
"accessKey": relay_info.access_key,
"expiresOn": relay_info.expires_on
}
})
result_bytes = relay_info_string.encode("ascii")
enc = base64.b64encode(result_bytes)
base64_result_string = enc.decode("ascii")
return base64_result_string
2 changes: 2 additions & 0 deletions src/ssh/azext_ssh/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
CLEANUP_TOTAL_TIME_LIMIT_IN_SECONDS = 120
CLEANUP_TIME_INTERVAL_IN_SECONDS = 10
CLEANUP_AWAIT_TERMINATION_IN_SECONDS = 30
RELAY_INFO_MAXIMUM_DURATION_IN_SECONDS = 3600
PROXY_UNSUPPORTED_ARCH_FAULT_TYPE = 'client-proxy-unsupported-architecture-error'
PROXY_UNSUPPORTED_OS_FAULT_TYPE = 'client-proxy-unsupported-os-error'
PROXY_DOWNLOAD_FAILED_FAULT_TYPE = 'client-proxy-download-failed-error'
LIST_CREDENTIALS_FAILED_FAULT_TYPE = 'get-relay-information-failed-error'

Loading

0 comments on commit b6e4cb6

Please sign in to comment.