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

[ssh] Add support to "Microsoft.ConnectedVMwarevSphere/virtualMachines" resource type #5367

Merged
merged 17 commits into from
Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from 12 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
10 changes: 10 additions & 0 deletions src/ssh/azext_ssh/_client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,13 @@ def cf_connectedmachine_cl(cli_ctx, *_):

def cf_machine(cli_ctx, *_):
return cf_connectedmachine_cl(cli_ctx).machines


def cf_connectedvmware_cl(cli_ctx, *_):
from azext_ssh.vendored_sdks.connectedvmware import AzureArcVMwareManagementServiceAPI
return get_mgmt_service_client(cli_ctx,
AzureArcVMwareManagementServiceAPI)
Comment on lines +35 to +37
Copy link
Contributor

@zhoxing-ms zhoxing-ms Oct 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please define a CustomResourceType for AzureArcVMwareManagementServiceAPI, such as:

CUSTOM_MGMT_STORAGE = CustomResourceType('azext_storage_preview.vendored_sdks.azure_mgmt_storage',
'StorageManagementClient')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zhoxing-ms Why do we need to use this custom resource type? do I need to use it for the other clients in the same file as well? (hybrid connectivity and connected machine clients)

Copy link
Contributor Author

@vthiebaut10 vthiebaut10 Oct 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zhoxing-ms when I try to use that I get this error: azure.cli.core.profiles._shared.APIVersionException: Unable to get API version for type '<azure.cli.core.profiles._shared.CustomResourceType object at 0x00000267F5ADE250>' in profile 'latest

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zhoxing-ms I pushed the changes you requested, but this error still happens. Do you know what might cause that?

Copy link
Contributor

@zhoxing-ms zhoxing-ms Oct 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vthiebaut10 I recommended you define CustomResourceType because the parameter of method get_mgmt_service_client should be passed in client_or_resource_type, I personally think it will be more in line with the code specification.
But it seems that AzureArcVMwareManagementServiceAPI is non-versioned client, so it is not suitable to define as CustomResourceType. So please use AzureArcVMwareManagementServiceAPI directly as before , thanks~

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zhoxing-ms I reverted the changes.



def cf_vmware(cli_ctx, *_):
return cf_connectedvmware_cl(cli_ctx).virtual_machines
18 changes: 15 additions & 3 deletions src/ssh/azext_ssh/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ def load_arguments(self, _):
help='Path to a certificate file used for authentication when using local user credentials.')
c.argument('port', options_list=['--port'], help='SSH port')
c.argument('resource_type', options_list=['--resource-type'],
help='Resource type should be either Microsoft.Compute or Microsoft.HybridCompute',
completer=["Microsoft.HybridCompute", "Microsoft.Compute"])
help=('Resource type should be either Microsoft.Compute/virtualMachines, '
'Microsoft.HybridCompute/machines, '
'or Microsoft.ConnectedVMwareSphere/virtualMachines.'),
completer=['Microsoft.Compute/virtualMachines', 'Microsoft.HybridCompute/machines',
'Microsoft.ConnectedVMwareSphere/virtualMachines'])
c.argument('ssh_client_folder', options_list=['--ssh-client-folder'],
help='Folder path that contains ssh executables (ssh.exe, ssh-keygen.exe, etc). '
'Default to ssh pre-installed if not provided.')
Expand Down Expand Up @@ -52,7 +55,11 @@ def load_arguments(self, _):
help='Folder where new generated keys will be stored.')
c.argument('port', options_list=['--port'], help='SSH Port')
c.argument('resource_type', options_list=['--resource-type'],
help='Resource type should be either Microsoft.Compute or Microsoft.HybridCompute')
help=('Resource type should be either Microsoft.Compute/virtualMachines, '
'Microsoft.HybridCompute/machines, '
'or Microsoft.ConnectedVMwareSphere/virtualMachines.'),
completer=['Microsoft.Compute/virtualMachines', 'Microsoft.HybridCompute/machines',
'Microsoft.ConnectedVMwareSphere/virtualMachines'])
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. '
Expand All @@ -79,6 +86,11 @@ def load_arguments(self, _):
help='The username for a local user')
c.argument('cert_file', options_list=['--certificate-file', '-c'], help='Path to certificate file')
c.argument('port', options_list=['--port'], help='Port to connect to on the remote host.')
c.argument('resource_type', options_list=['--resource-type'],
help=('Resource type should be either Microsoft.HybridCompute/machines '
'or Microsoft.ConnectedVMwareSphere/virtualMachines.'),
completer=['Microsoft.HybridCompute/machines',
'Microsoft.ConnectedVMwareSphere/virtualMachines'])
c.argument('ssh_client_folder', options_list=['--ssh-client-folder'],
help='Folder path that contains ssh executables (ssh.exe, ssh-keygen.exe, etc). '
'Default to ssh pre-installed if not provided.')
Expand Down
18 changes: 11 additions & 7 deletions src/ssh/azext_ssh/connectivity_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@


# 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):
def get_relay_information(cmd, resource_group, vm_name, resource_type, certificate_validity_in_seconds):
from azext_ssh._client_factory import cf_endpoint
client = cf_endpoint(cmd.cli_ctx)

Expand All @@ -39,16 +39,18 @@ def get_relay_information(cmd, resource_group, vm_name, certificate_validity_in_
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)
resource_type=resource_type, 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 ResourceNotFoundError:
logger.debug("Default Endpoint couldn't be found. Trying to create Default Endpoint.")
_create_default_endpoint(cmd, resource_group, vm_name, client)
_create_default_endpoint(cmd, resource_group, vm_name, resource_type, client)
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)
resource_type=resource_type, 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:
Expand All @@ -58,12 +60,14 @@ def get_relay_information(cmd, resource_group, vm_name, certificate_validity_in_
return result


def _create_default_endpoint(cmd, resource_group, vm_name, client):
def _create_default_endpoint(cmd, resource_group, vm_name, resource_type, client):
namespace = resource_type.split('/', 1)[0]
arc_type = resource_type.split('/', 1)[1]
az_resource_id = resource_id(subscription=get_subscription_id(cmd.cli_ctx), resource_group=resource_group,
namespace="Microsoft.HybridCompute", type="machines", name=vm_name)
namespace=namespace, type=arc_type, name=vm_name)
endpoint_resource = {"id": az_resource_id, "type_properties_type": "default"}
try:
client.create_or_update(resource_group, vm_name, "default", endpoint_resource)
client.create_or_update(resource_group, vm_name, resource_type, "default", endpoint_resource)
except Exception as e:
colorama.init()
raise azclierror.UnauthorizedError(f"Unable to create Default Endpoint for {vm_name} in {resource_group}."
Expand Down
32 changes: 32 additions & 0 deletions src/ssh/azext_ssh/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,35 @@
RECOMMENDATION_RESOURCE_NOT_FOUND = (Fore.YELLOW + "Please ensure the active subscription is set properly "
"and resource exists." + Style.RESET_ALL)
RDP_TERMINATE_SSH_WAIT_TIME_IN_SECONDS = 30

ARC_RESOURCE_TYPE_PLACEHOLDER = "arc_resource_type_placeholder"

SUPPORTED_RESOURCE_TYPES = ["microsoft.hybridcompute/machines",
"microsoft.compute/virtualmachines",
"microsoft.connectedvmwarevsphere/virtualmachines",
"microsoft.scvmm/virtualmachines",
"microsoft.azurestackhci/virtualmachines"]

# Old version incorrectly used resource providers instead of resource type.
# Will continue to support to avoid breaking backwards compatibility.
LEGACY_SUPPORTED_RESOURCE_TYPES = ["microsoft.hybridcompute",
"microsoft.compute",
"microsoft.connectedvmwarevsphere",
"microsoft.scvmm",
"microsoft.azurestackhci"]

RESOURCE_PROVIDER_TO_RESOURCE_TYPE = {
"microsoft.hybridcompute": "Microsoft.HybridCompute/machines",
"microsoft.compute": "Microsoft.Compute/virtualMachines",
"microsoft.connectedvmwarevsphere": "Microsoft.ConnectedVMwarevSphere/virtualMachines",
"microsoft.azurestackhci": "Microsoft.AzureStackHCI/virtualMachines",
"microsoft.scvmm": "Microsoft.ScVmm/virtualMachines"
}

RESOURCE_TYPE_LOWER_CASE_TO_CORRECT_CASE = {
"microsoft.hybridcompute/machines": "Microsoft.HybridCompute/machines",
"microsoft.compute/virtualmachines": "Microsoft.Compute/virtualMachines",
"microsoft.connectedvmwarevsphere/virtualmachines": "Microsoft.ConnectedVMwarevSphere/virtualMachines",
"microsoft.scvmm/virtualmachines": "Microsoft.ScVmm/virtualMachines",
"microsoft.azurestackhci/virtualmachines": "Microsoft.AzureStackHCI/virtualMachines"
}
148 changes: 24 additions & 124 deletions src/ssh/azext_ssh/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,9 @@
import platform
import oschmod

import colorama

from knack import log
from azure.cli.core import azclierror
from azure.cli.core import telemetry
from azure.core.exceptions import ResourceNotFoundError, HttpResponseError
from azure.cli.core.style import Style, print_styled_text

from . import ip_utils
Expand All @@ -27,6 +24,8 @@
from . import ssh_info
from . import file_utils
from . import constants as const
from . import resource_type_utils
from . import target_os_utils

logger = log.get_logger(__name__)

Expand Down Expand Up @@ -61,7 +60,9 @@ def ssh_vm(cmd, resource_group_name=None, vm_name=None, ssh_ip=None, public_key_
private_key_file, use_private_ip, local_user, cert_file, port,
ssh_client_folder, ssh_args, delete_credentials, resource_type,
ssh_proxy_folder, credentials_folder, winrdp)
ssh_session.resource_type = _decide_resource_type(cmd, ssh_session)
ssh_session.resource_type = resource_type_utils.decide_resource_type(cmd, ssh_session)
target_os_utils.handle_target_os_type(cmd, ssh_session)

_do_ssh_op(cmd, ssh_session, op_call)


Expand All @@ -81,7 +82,8 @@ def ssh_config(cmd, config_path, resource_group_name=None, vm_name=None, ssh_ip=
resource_type, credentials_folder, ssh_proxy_folder, ssh_client_folder)
op_call = ssh_utils.write_ssh_config

config_session.resource_type = _decide_resource_type(cmd, config_session)
config_session.resource_type = resource_type_utils.decide_resource_type(cmd, config_session)
target_os_utils.handle_target_os_type(cmd, config_session)

# if the folder doesn't exist, this extension won't create a new one.
config_folder = os.path.dirname(config_session.config_path)
Expand Down Expand Up @@ -139,11 +141,14 @@ def ssh_cert(cmd, cert_path=None, public_key_file=None, ssh_client_folder=None):


def ssh_arc(cmd, resource_group_name=None, vm_name=None, public_key_file=None, private_key_file=None,
local_user=None, cert_file=None, port=None, ssh_client_folder=None, delete_credentials=False,
ssh_proxy_folder=None, winrdp=False, ssh_args=None):
local_user=None, cert_file=None, port=None, resource_type=None, ssh_client_folder=None,
delete_credentials=False, ssh_proxy_folder=None, winrdp=False, ssh_args=None):

if not resource_type:
resource_type = const.ARC_RESOURCE_TYPE_PLACEHOLDER

ssh_vm(cmd, resource_group_name, vm_name, None, public_key_file, private_key_file, False, local_user, cert_file,
port, ssh_client_folder, delete_credentials, "Microsoft.HybridCompute", ssh_proxy_folder, winrdp, ssh_args)
port, ssh_client_folder, delete_credentials, resource_type, ssh_proxy_folder, winrdp, ssh_args)


def _do_ssh_op(cmd, op_info, op_call):
Expand Down Expand Up @@ -183,7 +188,8 @@ def _do_ssh_op(cmd, op_info, op_call):
if op_info.is_arc():
op_info.proxy_path = connectivity_utils.get_client_side_proxy(op_info.ssh_proxy_folder)
op_info.relay_info = connectivity_utils.get_relay_information(cmd, op_info.resource_group_name,
op_info.vm_name, cert_lifetime)
op_info.vm_name, op_info.resource_type,
cert_lifetime)
except Exception as e:
if delete_keys or delete_cert:
logger.debug("An error occured before operation concluded. Deleting generated keys: %s %s %s",
Expand Down Expand Up @@ -260,10 +266,15 @@ def _prepare_jwk_data(public_key_file):


def _assert_args(resource_group, vm_name, ssh_ip, resource_type, cert_file, username):
if resource_type and resource_type.lower() != "microsoft.compute" \
and resource_type.lower() != "microsoft.hybridcompute":
raise azclierror.InvalidArgumentValueError("--resource-type must be either \"Microsoft.Compute\" "
"for Azure VMs or \"Microsoft.HybridCompute\" for Arc Servers.")

if resource_type and \
resource_type.lower() not in const.SUPPORTED_RESOURCE_TYPES and \
resource_type.lower() not in const.LEGACY_SUPPORTED_RESOURCE_TYPES and \
resource_type != const.ARC_RESOURCE_TYPE_PLACEHOLDER:
raise azclierror.InvalidArgumentValueError("--resource-type must be either "
"\"Microsoft.Compute/virtualMachines\", "
"\"Microsoft.HybridCompute/machines\", "
"or \"Microsoft.ConnectedVMwareSphere/virtualMachines\".")

if not (resource_group or vm_name or ssh_ip):
raise azclierror.RequiredArgumentMissingError(
Expand Down Expand Up @@ -352,114 +363,3 @@ def _get_modulus_exponent(public_key_file):
exponent = parser.exponent

return modulus, exponent


def _decide_resource_type(cmd, op_info):
# If the user provides an IP address the target will be treated as an Azure VM even if it is an
# Arc Server. Which just means that the Connectivity Proxy won't be used to establish connection.
is_arc_server = False
is_azure_vm = False

if op_info.ip:
is_azure_vm = True
vm = None

elif op_info.resource_type:
if op_info.resource_type.lower() == "microsoft.hybridcompute":
arc, arc_error, is_arc_server = _check_if_arc_server(cmd, op_info.resource_group_name, op_info.vm_name)
if not is_arc_server:
colorama.init()
if isinstance(arc_error, ResourceNotFoundError):
raise azclierror.ResourceNotFoundError(f"The resource {op_info.vm_name} in the resource group "
f"{op_info.resource_group_name} was not found.",
const.RECOMMENDATION_RESOURCE_NOT_FOUND)
raise azclierror.BadRequestError("Unable to determine that the target machine is an Arc Server. "
f"Error:\n{str(arc_error)}", const.RECOMMENDATION_RESOURCE_NOT_FOUND)

elif op_info.resource_type.lower() == "microsoft.compute":
vm, vm_error, is_azure_vm = _check_if_azure_vm(cmd, op_info.resource_group_name, op_info.vm_name)
if not is_azure_vm:
colorama.init()
if isinstance(vm_error, ResourceNotFoundError):
raise azclierror.ResourceNotFoundError(f"The resource {op_info.vm_name} in the resource group "
f"{op_info.resource_group_name} was not found.",
const.RECOMMENDATION_RESOURCE_NOT_FOUND)
raise azclierror.BadRequestError("Unable to determine that the target machine is an Azure VM. "
f"Error:\n{str(vm_error)}", const.RECOMMENDATION_RESOURCE_NOT_FOUND)

else:
vm, vm_error, is_azure_vm = _check_if_azure_vm(cmd, op_info.resource_group_name, op_info.vm_name)
arc, arc_error, is_arc_server = _check_if_arc_server(cmd, op_info.resource_group_name, op_info.vm_name)

if is_azure_vm and is_arc_server:
colorama.init()
raise azclierror.BadRequestError(f"{op_info.resource_group_name} has Azure VM and Arc Server with the "
f"same name: {op_info.vm_name}.",
colorama.Fore.YELLOW + "Please provide a --resource-type." +
colorama.Style.RESET_ALL)
if not is_azure_vm and not is_arc_server:
colorama.init()
if isinstance(arc_error, ResourceNotFoundError) and isinstance(vm_error, ResourceNotFoundError):
raise azclierror.ResourceNotFoundError(f"The resource {op_info.vm_name} in the resource group "
f"{op_info.resource_group_name} was not found. ",
const.RECOMMENDATION_RESOURCE_NOT_FOUND)
raise azclierror.BadRequestError("Unable to determine the target machine type as Azure VM or "
f"Arc Server. Errors:\n{str(arc_error)}\n{str(vm_error)}",
const.RECOMMENDATION_RESOURCE_NOT_FOUND)

# Note: We are not able to determine the os of the target if the user only provides an IP address.
os_type = None
if is_azure_vm and vm and vm.storage_profile and vm.storage_profile.os_disk and vm.storage_profile.os_disk.os_type:
os_type = vm.storage_profile.os_disk.os_type

if is_arc_server and arc and arc.properties and arc.properties and arc.properties.os_name:
os_type = arc.properties.os_name

if os_type:
telemetry.add_extension_event('ssh', {'Context.Default.AzureCLI.TargetOSType': os_type})

# Note 2: This is a temporary check while AAD login is not enabled for Windows.
if os_type and os_type.lower() == 'windows' and not op_info.local_user:
colorama.init()
raise azclierror.RequiredArgumentMissingError("SSH Login using AAD credentials is not currently supported "
"for Windows.",
colorama.Fore.YELLOW + "Please provide --local-user." +
colorama.Style.RESET_ALL)

target_resource_type = "Microsoft.Compute"
if is_arc_server:
target_resource_type = "Microsoft.HybridCompute"
telemetry.add_extension_event('ssh', {'Context.Default.AzureCLI.TargetResourceType': target_resource_type})

return target_resource_type


def _check_if_azure_vm(cmd, resource_group_name, vm_name):
from azure.cli.core.commands import client_factory
from azure.cli.core import profiles
vm = None
try:
compute_client = client_factory.get_mgmt_service_client(cmd.cli_ctx, profiles.ResourceType.MGMT_COMPUTE)
vm = compute_client.virtual_machines.get(resource_group_name, vm_name)
except ResourceNotFoundError as e:
return None, e, False
# If user is not authorized to get the VM, it will throw a HttpResponseError
except HttpResponseError as e:
return None, e, False

return vm, None, True


def _check_if_arc_server(cmd, resource_group_name, vm_name):
from azext_ssh._client_factory import cf_machine
client = cf_machine(cmd.cli_ctx)
arc = None
try:
arc = client.get(resource_group_name=resource_group_name, machine_name=vm_name)
except ResourceNotFoundError as e:
return None, e, False
# If user is not authorized to get the arc server, it will throw a HttpResponseError
except HttpResponseError as e:
return None, e, False

return arc, None, True
Loading