Skip to content

Commit ec69d77

Browse files
rhkodiakrhoover
andauthored
[Serial-Console]: az serial-console connect: Change to use different region for url calls when custom storage account firewalls are enabled (#5398)
* Updated code to enable the cli to connect to serial-console over different regions * Updated arguments that are passed around for the clients * Change the code to resolve the region name at the beginning of the cli command * Moved the _arm_endpoints.py file up two directories to resolve import issues * Fix code issues where the serial-console wasn't using the storage_url correctly * Resolve Pylint issues * Resolve Pylint issues * Resolve Pylint issues * Resolve Pylint issue with to few public methods * Update the version and release notes * Change release note verbiage * Fix live tests * Fix formatting issue * Add new recording files from running live tests * Fix spacing issue and restart failed tests * Change print statement Changed the print statement to use the logger.debug() option to only output the boot_diagnostics section for debugging * Add logger import Added the logger import to correct the build failure Co-authored-by: rhoover <[email protected]>
1 parent 54b84f2 commit ec69d77

File tree

9 files changed

+4013
-2851
lines changed

9 files changed

+4013
-2851
lines changed

src/serial-console/HISTORY.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
Release History
22
===============
33

4+
0.1.3
5+
++++++
6+
* Change to use different region for url calls when custom storage account firewalls are enabled
7+
48
0.1.2
59
++++++
610
* Change to make custom boot diagnostics optional
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
7+
class ArmEndpoints: # pylint: disable=too-few-public-methods
8+
region_prefix_pairings = {'australiacentral': 'australiaeast',
9+
'australiaeast': 'australiacentral',
10+
'brazilsouth': 'brazilsoutheast',
11+
'brazilsoutheast': 'brazilsouth',
12+
'canadacentral': 'canadaeast',
13+
'canadaeast': 'canadacentral',
14+
'centralindia': 'southindia',
15+
'centralus': 'westcentralus',
16+
'centraluseuap': 'eastus2euap',
17+
'eastasia': 'southeastasia',
18+
'eastus2': 'westus2', # pairing eastus2 + westus2 ensure that INT works as expected
19+
'eastus2euap': 'centraluseuap',
20+
'francecentral': 'francesouth',
21+
'francesouth': 'francecentral',
22+
'germanynorth': 'germanywestcentral',
23+
'germanywestcentral': 'germanynorth',
24+
'japaneast': 'japanwest',
25+
'japanwest': 'japaneast',
26+
'koreacentral': 'koreasouth',
27+
'koreasouth': 'koreacentral',
28+
'northeurope': 'westeurope',
29+
'norwayeast': 'norwaywest',
30+
'norwaywest': 'norwayeast',
31+
# 'southafricanorth': 'southafricawest' is not yet deployed
32+
'southeastasia': 'eastasia',
33+
'southindia': 'centralindia',
34+
'swedencentral': 'swedensouth',
35+
'swedensouth': 'swedencentral',
36+
'switzerlandnorth': 'switzerlandwest',
37+
'switzerlandwest': 'switzerlandnorth',
38+
'uaecentral': 'uaenorth',
39+
'uaenorth': 'uaecentral',
40+
'uksouth': 'ukwest',
41+
'ukwest': 'uksouth',
42+
'westcentralus': 'centralus',
43+
'westeurope': 'northeurope',
44+
'westus2': 'eastus2',
45+
'usgovarizona': 'usgoveast', # usgoveast == usgovvirginia
46+
'usgovvirginia': 'usgovsw', # usgovsw == usgovarizona
47+
}

src/serial-console/azext_serialconsole/_client_factory.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,27 @@
33
# Licensed under the MIT License. See License.txt in the project root for license information.
44
# --------------------------------------------------------------------------------------------
55

6+
from azure.cli.core.profiles import ResourceType
7+
68

79
def _compute_client_factory(cli_ctx, **kwargs):
8-
from azure.cli.core.profiles import ResourceType
910
from azure.cli.core.commands.client_factory import get_mgmt_service_client
1011
return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_COMPUTE,
1112
subscription_id=kwargs.get('subscription_id'),
1213
aux_subscriptions=kwargs.get('aux_subscriptions'))
1314

1415

15-
def cf_serialconsole(cli_ctx, *_):
16+
def cf_serialconsole(cli_ctx, **kwargs):
1617
from azure.cli.core.commands.client_factory import get_mgmt_service_client
1718
from azext_serialconsole.vendored_sdks.serialconsole import MicrosoftSerialConsoleClient
1819
return get_mgmt_service_client(cli_ctx,
19-
MicrosoftSerialConsoleClient)
20+
MicrosoftSerialConsoleClient, **kwargs)
21+
2022

23+
def cf_serial_port(cli_ctx, **kwargs):
24+
return cf_serialconsole(cli_ctx, **kwargs).serial_ports
2125

22-
def cf_serial_port(cli_ctx, *_):
23-
return cf_serialconsole(cli_ctx).serial_ports
26+
27+
def storage_client_factory(cli_ctx, *_):
28+
from azure.cli.core.commands.client_factory import get_mgmt_service_client
29+
return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_STORAGE)

src/serial-console/azext_serialconsole/custom.py

Lines changed: 118 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def prompt(self, getch, message):
132132
c = getch()
133133
self.hide_cursor(buffer=False)
134134
for _ in range(lines):
135-
self.clear_line(buffer=False)
135+
# self.clear_line(buffer=False)
136136
self.cursor_up(buffer=False)
137137
self.set_cursor_horizontal_position(col, buffer=False)
138138
self.show_cursor(buffer=False)
@@ -198,8 +198,8 @@ def _getch_windows(self):
198198

199199
class Terminal:
200200
ERROR_MESSAGE = "Unable to configure terminal."
201-
RECOMENDATION = ("Make sure that app in running in a terminal on a Windows 10 "
202-
"or Unix based machine. Versions earlier than Windows 10 are not supported.")
201+
RECOMMENDATION = ("Make sure that app in running in a terminal on a Windows 10 "
202+
"or Unix based machine. Versions earlier than Windows 10 are not supported.")
203203

204204
def __init__(self):
205205
self.win_original_out_mode = None
@@ -232,7 +232,7 @@ def configure_terminal(self):
232232
if (not kernel32.GetConsoleMode(self.win_out, ctypes.byref(dw_original_out_mode)) or
233233
not kernel32.GetConsoleMode(self.win_in, ctypes.byref(dw_original_in_mode))):
234234
quitapp(error_message=Terminal.ERROR_MESSAGE,
235-
error_recommendation=Terminal.RECOMENDATION, error_func=UnclassifiedUserFault)
235+
error_recommendation=Terminal.RECOMMENDATION, error_func=UnclassifiedUserFault)
236236

237237
self.win_original_out_mode = dw_original_out_mode.value
238238
self.win_original_in_mode = dw_original_in_mode.value
@@ -244,15 +244,15 @@ def configure_terminal(self):
244244
if (not kernel32.SetConsoleMode(self.win_out, dw_out_mode) or
245245
not kernel32.SetConsoleMode(self.win_in, dw_in_mode)):
246246
quitapp(error_message=Terminal.ERROR_MESSAGE,
247-
error_recommendation=Terminal.RECOMENDATION, error_func=UnclassifiedUserFault)
247+
error_recommendation=Terminal.RECOMMENDATION, error_func=UnclassifiedUserFault)
248248
else:
249249
try:
250250
import tty
251251
import termios # pylint: disable=import-error
252252
fd = sys.stdin.fileno()
253253
except (ModuleNotFoundError, ValueError):
254254
quitapp(error_message=Terminal.ERROR_MESSAGE,
255-
error_recommendation=Terminal.RECOMENDATION, error_func=UnclassifiedUserFault)
255+
error_recommendation=Terminal.RECOMMENDATION, error_func=UnclassifiedUserFault)
256256

257257
self.unix_original_mode = termios.tcgetattr(fd)
258258
tty.setraw(fd)
@@ -277,7 +277,13 @@ def revert_terminal(self):
277277

278278
class SerialConsole:
279279
def __init__(self, cmd, resource_group_name, vm_vmss_name, vmss_instanceid):
280-
client = cf_serial_port(cmd.cli_ctx)
280+
result, storage_account_region = get_region_from_storage_account(cmd.cli_ctx, resource_group_name,
281+
vm_vmss_name, vmss_instanceid)
282+
if storage_account_region is not None:
283+
kwargs = {'storage_account_region': storage_account_region}
284+
else:
285+
kwargs = {}
286+
client = cf_serial_port(cmd.cli_ctx, **kwargs)
281287
if vmss_instanceid is None:
282288
self.connect_func = lambda: client.connect(
283289
resource_group_name=resource_group_name,
@@ -365,7 +371,7 @@ def connect_loading_message_linux():
365371
chars_copy = chars.copy()
366372
chars_copy[indx] = "\u25A0"
367373
squares = " ".join(chars_copy)
368-
PC.clear_line()
374+
# PC.clear_line()
369375
PC.print("Connecting to console of VM " +
370376
squares, color=PrintClass.CYAN)
371377
PC.show_cursor()
@@ -457,7 +463,7 @@ def connect_thread():
457463
GV.websocket_instance.run_forever(skip_utf8_validation=True)
458464
else:
459465
GV.loading = False
460-
message = ("\r\nAn unexpected error occured. Could not establish connection to VM or VMSS. "
466+
message = ("\r\nAn unexpected error occurred. Could not establish connection to VM or VMSS. "
461467
"Check network connection and press \"Enter\" to try again...")
462468
PC.print(message, color=PrintClass.RED)
463469

@@ -524,6 +530,7 @@ def connect_and_send_admin_command(self, command, arg_characters=None):
524530
elif command == "sysrq" and arg_characters is not None:
525531
def wrapper():
526532
return self.send_sys_rq(arg_characters)
533+
527534
func = wrapper
528535
success_message = "Successfully sent SysRq command\r\n"
529536
failure_message = "Failed to send SysRq command. Make sure the input only contains numbers and letters.\r\n"
@@ -563,14 +570,18 @@ def on_message(ws, _):
563570
error_message, recommendation=recommendation)
564571
else:
565572
GV.loading = False
566-
error_message = "An unexpected error occured. Could not establish connection to VM or VMSS."
573+
error_message = "An unexpected error occurred. Could not establish connection to VM or VMSS."
567574
recommendation = "Check network connection and try again."
568575
raise ResourceNotFoundError(
569576
error_message, recommendation=recommendation)
570577

571578

572-
def check_serial_console_enabled(cli_ctx):
573-
client = cf_serialconsole(cli_ctx)
579+
def check_serial_console_enabled(cli_ctx, storage_account_region=None):
580+
if storage_account_region is not None:
581+
kwargs = {'storage_account_region': storage_account_region}
582+
else:
583+
kwargs = {}
584+
client = cf_serialconsole(cli_ctx, **kwargs)
574585
result = client.get_console_status().additional_properties
575586
if ("properties" in result and "disabled" in result["properties"] and
576587
not result["properties"]["disabled"]):
@@ -581,11 +592,11 @@ def check_serial_console_enabled(cli_ctx):
581592

582593

583594
def check_resource(cli_ctx, resource_group_name, vm_vmss_name, vmss_instanceid):
584-
check_serial_console_enabled(cli_ctx)
585-
client = _compute_client_factory(cli_ctx)
595+
result, storage_account_region = get_region_from_storage_account(cli_ctx, resource_group_name, vm_vmss_name,
596+
vmss_instanceid)
597+
check_serial_console_enabled(cli_ctx, storage_account_region)
598+
586599
if vmss_instanceid:
587-
result = client.virtual_machine_scale_set_vms.get_instance_view(
588-
resource_group_name, vm_vmss_name, vmss_instanceid)
589600
if 'osName' in result.additional_properties and "windows" in result.additional_properties['osName'].lower():
590601
GV.os_is_windows = True
591602

@@ -596,32 +607,7 @@ def check_resource(cli_ctx, resource_group_name, vm_vmss_name, vmss_instanceid):
596607
recommendation = 'Use "az vmss start" to start the Virtual Machine.'
597608
raise AzureConnectionError(
598609
error_message, recommendation=recommendation)
599-
600-
if result.boot_diagnostics is None:
601-
error_message = ("Azure Serial Console requires boot diagnostics to be enabled.")
602-
recommendation = ('Use "az vmss update --name MyScaleSet --resource-group MyResourceGroup --set '
603-
'virtualMachineProfile.diagnosticsProfile="{\\"bootDiagnostics\\": {\\"Enabled\\" : '
604-
'\\"True\\",\\"StorageUri\\" : null}}"" to enable boot diagnostics. '
605-
'You can replace "null" with a custom storage account '
606-
'\\"https://mystor.blob.windows.net/"\\. Then run "az vmss update-instances -n '
607-
'MyScaleSet -g MyResourceGroup --instance-ids *".')
608-
raise AzureConnectionError(
609-
error_message, recommendation=recommendation)
610610
else:
611-
try:
612-
result = client.virtual_machines.get(
613-
resource_group_name, vm_vmss_name, expand='instanceView')
614-
except ComputeClientResourceNotFoundError as e:
615-
try:
616-
client.virtual_machine_scale_sets.get(
617-
resource_group_name, vm_vmss_name)
618-
except ComputeClientResourceNotFoundError:
619-
raise e from e
620-
error_message = e.message
621-
recommendation = ("We found that you specified a Virtual Machine Scale Set and not a VM. "
622-
"Use the --instance-id parameter to select the VMSS instance you want to connect to.")
623-
raise ResourceNotFoundError(
624-
error_message, recommendation=recommendation) from e
625611
if (result.instance_view is not None and
626612
result.instance_view.os_name is not None and
627613
"windows" in result.instance_view.os_name.lower()):
@@ -640,16 +626,6 @@ def check_resource(cli_ctx, resource_group_name, vm_vmss_name, vmss_instanceid):
640626
raise AzureConnectionError(
641627
error_message, recommendation=recommendation)
642628

643-
if (result.diagnostics_profile is None or
644-
result.diagnostics_profile.boot_diagnostics is None or
645-
not result.diagnostics_profile.boot_diagnostics.enabled):
646-
error_message = ("Azure Serial Console requires boot diagnostics to be enabled.")
647-
recommendation = ('Use "az vm boot-diagnostics enable --name MyVM --resource-group MyResourceGroup" '
648-
'to enable boot diagnostics. You can specify a custom storage account with the '
649-
'parameter "--storage https://mystor.blob.windows.net/".')
650-
raise AzureConnectionError(
651-
error_message, recommendation=recommendation)
652-
653629

654630
def connect_serialconsole(cmd, resource_group_name, vm_vmss_name, vmss_instanceid=None):
655631
check_resource(cmd.cli_ctx, resource_group_name,
@@ -695,3 +671,94 @@ def enable_serialconsole(cmd):
695671
def disable_serialconsole(cmd):
696672
client = cf_serialconsole(cmd.cli_ctx)
697673
return client.disable_console()
674+
675+
676+
def get_region_from_storage_account(cli_ctx, resource_group_name, vm_vmss_name, vmss_instanceid):
677+
from azext_serialconsole._client_factory import storage_client_factory
678+
from knack.log import get_logger
679+
680+
logger = get_logger(__name__)
681+
result = None
682+
storage_account_region = None
683+
client = _compute_client_factory(cli_ctx)
684+
scf = storage_client_factory(cli_ctx)
685+
686+
if vmss_instanceid:
687+
result_data = client.virtual_machine_scale_set_vms.get_instance_view(
688+
resource_group_name, vm_vmss_name, vmss_instanceid)
689+
result = result_data
690+
691+
if result_data.boot_diagnostics is None:
692+
error_message = "Azure Serial Console requires boot diagnostics to be enabled."
693+
recommendation = ('Use "az vmss update --name MyScaleSet --resource-group MyResourceGroup --set '
694+
'virtualMachineProfile.diagnosticsProfile="{\\"bootDiagnostics\\": {\\"Enabled\\" : '
695+
'\\"True\\",\\"StorageUri\\" : null}}"" to enable boot diagnostics. '
696+
'You can replace "null" with a custom storage account '
697+
'\\"https://mystor.blob.windows.net/"\\. Then run "az vmss update-instances -n '
698+
'MyScaleSet -g MyResourceGroup --instance-ids *".')
699+
raise AzureConnectionError(
700+
error_message, recommendation=recommendation)
701+
else:
702+
if result.boot_diagnostics is not None:
703+
logger.debug(result.boot_diagnostics)
704+
if result.boot_diagnostics.console_screenshot_blob_uri is not None:
705+
storage_account_url = result.boot_diagnostics.console_screenshot_blob_uri
706+
storage_account_region = get_storage_account_info(storage_account_url, resource_group_name, scf)
707+
else:
708+
try:
709+
result_data = client.virtual_machines.get(
710+
resource_group_name, vm_vmss_name, expand='instanceView')
711+
result = result_data
712+
except ComputeClientResourceNotFoundError as e:
713+
try:
714+
client.virtual_machine_scale_sets.get(resource_group_name, vm_vmss_name)
715+
except ComputeClientResourceNotFoundError:
716+
raise e from e
717+
error_message = e.message
718+
recommendation = ("We found that you specified a Virtual Machine Scale Set and not a VM. "
719+
"Use the --instance-id parameter to select the VMSS instance you want to connect to.")
720+
raise ResourceNotFoundError(
721+
error_message, recommendation=recommendation) from e
722+
723+
if (result.diagnostics_profile is None or
724+
result.diagnostics_profile.boot_diagnostics is None or
725+
not result.diagnostics_profile.boot_diagnostics.enabled):
726+
error_message = "Azure Serial Console requires boot diagnostics to be enabled."
727+
recommendation = ('Use "az vm boot-diagnostics enable --name MyVM --resource-group MyResourceGroup" '
728+
'to enable boot diagnostics. You can specify a custom storage account with the '
729+
'parameter "--storage https://mystor.blob.windows.net/".')
730+
raise AzureConnectionError(
731+
error_message, recommendation=recommendation)
732+
else:
733+
if result.diagnostics_profile is not None:
734+
if result.diagnostics_profile.boot_diagnostics is not None:
735+
storage_account_url = result.diagnostics_profile.boot_diagnostics.storage_uri
736+
storage_account_region = get_storage_account_info(storage_account_url, resource_group_name, scf)
737+
738+
return result, storage_account_region
739+
740+
741+
def get_storage_account_info(storage_account_url, resource_group_name, scf):
742+
from azext_serialconsole._arm_endpoints import ArmEndpoints
743+
744+
if storage_account_url is not None:
745+
storage_account = parse_storage_account_url(storage_account_url)
746+
if storage_account is not None:
747+
sa_result = scf.storage_accounts.get_properties(resource_group_name, storage_account)
748+
if (sa_result is not None and
749+
sa_result.network_rule_set is not None and
750+
len(sa_result.network_rule_set.ip_rules) > 0):
751+
return ArmEndpoints.region_prefix_pairings[sa_result.location]
752+
753+
return None
754+
755+
756+
def parse_storage_account_url(url):
757+
if url is not None:
758+
sa_list = url.split('.')
759+
if len(sa_list) > 0:
760+
sa_url = sa_list[0]
761+
sa_url = sa_url.replace("https://", "")
762+
return sa_url
763+
764+
return None

0 commit comments

Comments
 (0)