Skip to content

Commit 2590228

Browse files
Talos10Razvan Ivan
andauthored
Making admin commands work for serial console when the VM has a custom storage account attached (#9479)
* Making admin commands work for serial console when the VM has a custom storage account attached. * Updating the test recordings in order for the tests to pass in the pipeline --------- Co-authored-by: Razvan Ivan <[email protected]>
1 parent 13d5f49 commit 2590228

File tree

7 files changed

+2806
-2707
lines changed

7 files changed

+2806
-2707
lines changed

src/serial-console/HISTORY.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
Release History
22
===============
3+
1.0.0b3
4+
++++++
5+
* Fixed an issue where admin commands were not being sent when the VM was using a custom boot diagnostics storage account.
6+
37
1.0.0b2
48
++++++
59
* Changed to 2024 API version, fixes Disable API to track "properties". Essentially return to 2018 format

src/serial-console/azext_serialconsole/custom.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,9 @@ def wrapper():
551551
if self.load_websocket_url():
552552
def on_message(ws, _):
553553
GV.trycount += 1
554+
if GV.first_message:
555+
ws.send(self.access_token)
556+
GV.first_message = False
554557
if func():
555558
GV.loading = False
556559
GV.terminating_app = True

src/serial-console/azext_serialconsole/tests/latest/recordings/test_check_resource_VM.yaml

Lines changed: 1239 additions & 1210 deletions
Large diffs are not rendered by default.

src/serial-console/azext_serialconsole/tests/latest/recordings/test_check_resource_VMSS.yaml

Lines changed: 1435 additions & 1395 deletions
Large diffs are not rendered by default.

src/serial-console/azext_serialconsole/tests/latest/recordings/test_enable_disable.yaml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ interactions:
1515
Content-Type:
1616
- application/json
1717
User-Agent:
18-
- AZURECLI/2.67.0 azsdk-python-core/1.31.0 Python/3.12.7 (Windows-11-10.0.26100-SP0)
18+
- AZURECLI/2.81.0 azsdk-python-core/1.35.0 Python/3.12.3 (Linux-5.15.167.4-microsoft-standard-WSL2-x86_64-with-glibc2.39)
1919
method: POST
2020
uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.SerialConsole/consoleServices/default/disableConsole?api-version=2024-07-01
2121
response:
@@ -29,7 +29,7 @@ interactions:
2929
content-type:
3030
- application/json; charset=UTF-8
3131
date:
32-
- Thu, 21 Nov 2024 16:51:44 GMT
32+
- Wed, 17 Dec 2025 12:49:02 GMT
3333
expires:
3434
- '-1'
3535
pragma:
@@ -42,12 +42,14 @@ interactions:
4242
- nosniff
4343
x-frame-options:
4444
- deny
45+
x-ms-operation-identifier:
46+
- tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47,objectId=03039592-f720-4b7e-a2c4-fc19397bcba2/westeurope/0cf09d0b-d849-4c75-9bed-18a14590aa6f
4547
x-ms-ratelimit-remaining-subscription-global-writes:
4648
- '11999'
4749
x-ms-ratelimit-remaining-subscription-writes:
4850
- '799'
4951
x-msedge-ref:
50-
- 'Ref A: 467FB350BAF9492C96DFB004512EE7F1 Ref B: MNZ221060609021 Ref C: 2024-11-21T16:51:44Z'
52+
- 'Ref A: CFFAEE1F199D45EA98E8C1094AF0DE3A Ref B: AMS231032609017 Ref C: 2025-12-17T12:49:02Z'
5153
status:
5254
code: 200
5355
message: OK
@@ -67,7 +69,7 @@ interactions:
6769
Content-Type:
6870
- application/json
6971
User-Agent:
70-
- AZURECLI/2.67.0 azsdk-python-core/1.31.0 Python/3.12.7 (Windows-11-10.0.26100-SP0)
72+
- AZURECLI/2.81.0 azsdk-python-core/1.35.0 Python/3.12.3 (Linux-5.15.167.4-microsoft-standard-WSL2-x86_64-with-glibc2.39)
7173
method: POST
7274
uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.SerialConsole/consoleServices/default/enableConsole?api-version=2024-07-01
7375
response:
@@ -81,7 +83,7 @@ interactions:
8183
content-type:
8284
- application/json; charset=UTF-8
8385
date:
84-
- Thu, 21 Nov 2024 16:51:45 GMT
86+
- Wed, 17 Dec 2025 12:49:02 GMT
8587
expires:
8688
- '-1'
8789
pragma:
@@ -94,12 +96,14 @@ interactions:
9496
- nosniff
9597
x-frame-options:
9698
- deny
99+
x-ms-operation-identifier:
100+
- tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47,objectId=03039592-f720-4b7e-a2c4-fc19397bcba2/westeurope/ff9774cd-b1cf-492a-82a2-a8c13852eb61
97101
x-ms-ratelimit-remaining-subscription-global-writes:
98102
- '11999'
99103
x-ms-ratelimit-remaining-subscription-writes:
100104
- '799'
101105
x-msedge-ref:
102-
- 'Ref A: 01DB380606B146F6B7AA727925F4916A Ref B: MNZ221060618039 Ref C: 2024-11-21T16:51:45Z'
106+
- 'Ref A: C96BCE73671C4907B60C62F22C76EB1B Ref B: AMS231022012021 Ref C: 2025-12-17T12:49:02Z'
103107
status:
104108
code: 200
105109
message: OK

src/serial-console/azext_serialconsole/tests/latest/test_serialconsole_scenario.py

Lines changed: 114 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,8 @@ def test_enable_disable(self):
197197

198198
class SerialconsoleAdminCommandsTest(LiveScenarioTest):
199199

200-
def check_result(self, resource_group_name, vm_vmss_name, vmss_instanceid=None, message=""):
200+
def check_result(self, resource_group_name, vm_vmss_name, vmss_instanceid=None, message="", hasManagedStorageAccount=False):
201+
print("Checking serial console output for message: ", message)
201202
ARM_ENDPOINT = "https://management.azure.com"
202203
RP_PROVIDER = "Microsoft.SerialConsole"
203204
subscription_id = self.get_subscription_id()
@@ -215,21 +216,66 @@ def check_result(self, resource_group_name, vm_vmss_name, vmss_instanceid=None,
215216
headers = {'authorization': "Bearer " + access_token,
216217
'accept': application_json_format,
217218
'content-type': application_json_format}
218-
result = requests.post(connection_url, headers=headers)
219-
json_results = json.loads(result.text)
220-
self.assertTrue(result.status_code ==
221-
200 and "connectionString" in json_results)
219+
220+
postRetryCounter = 1
221+
222+
while True:
223+
result = requests.post(connection_url, headers=headers)
224+
json_results = json.loads(result.text)
225+
226+
if result.status_code == 200 and "connectionString" in json_results:
227+
break
228+
else:
229+
print("Failed to get connection string from serial console connect endpoint.")
230+
print("Status code: ", result.status_code)
231+
print("Response text: ", result.text)
232+
233+
if postRetryCounter > 3:
234+
self.fail("Failed to get connection string from serial console connect endpoint after retrying multiple times... Failing test. See status and response of retries in logs.")
235+
else:
236+
postRetryCounter += 1
237+
time.sleep(10)
238+
222239
websocket_url = json_results["connectionString"]
223240

224241
ws = websocket.WebSocket()
225242
ws.connect(websocket_url + "?authorization=" + access_token, timeout=30)
243+
print("WebSocket connected for verifying message.")
244+
print("Sleeping 60 seconds to allow 'Connecting...' message to appear.")
245+
time.sleep(60)
246+
247+
if not hasManagedStorageAccount:
248+
print("Sending access token to start custom storage account setup...")
249+
ws.send(access_token)
250+
print("Finished sending access token")
251+
226252
buffer = ""
253+
iter = 0
254+
print("Starting to read from WebSocket to find message...")
227255
while True:
228-
try:
229-
buffer += ws.recv()
230-
except (websocket.WebSocketTimeoutException, websocket.WebSocketConnectionClosedException):
256+
iter += 1
257+
print(f"Current timestamp: {time.strftime('%X')}, current iteration: {iter}/5")
258+
259+
while True:
260+
try:
261+
buffer += ws.recv()
262+
except (websocket.WebSocketTimeoutException, websocket.WebSocketConnectionClosedException):
263+
break
264+
265+
if message in buffer:
266+
print("Found message in buffer! Finished verification.")
231267
break
232-
268+
269+
print(f"Message not found yet in buffer, current buffer: {buffer}")
270+
271+
if iter >= 10:
272+
print("Max retries reached, exiting read loop.")
273+
break
274+
else:
275+
print("Sleeping 10 seconds before retrying...")
276+
time.sleep(10)
277+
278+
ws.close()
233279
assert message in buffer
234280

235281
@ResourceGroupPreparer(name_prefix='cli_test_serialconsole', location='westus2')
@@ -243,27 +289,13 @@ def test_send_sysrq_VMSS(self, resource_group, storage_account):
243289
'urn': 'Ubuntu2204',
244290
'loc': 'westus2'
245291
})
246-
self.cmd(
247-
'az vmss create -g {rg} -n {name} --image {urn} --instance-count 2 -l {loc} --orchestration-mode uniform')
248-
self.cmd('az vmss update --name {name} --resource-group {rg} --set virtualMachineProfile.diagnosticsProfile="{{\\"bootDiagnostics\\": {{\\"Enabled\\" : \\"True\\",\\"StorageUri\\":\\"https://{sa}.blob.core.windows.net/\\"}}}}"')
249-
result = self.cmd(
250-
'vmss list-instances --resource-group {rg} --name {name} --query "[].instanceId"').get_output_in_json()
251-
self.kwargs.update({'id': result[1]})
252-
self.cmd(
253-
'az vmss update-instances -g {rg} -n {name} --instance-ids {id}')
254-
time.sleep(60)
255-
for i in range(5):
256-
try:
257-
self.cmd('vmss get-instance-view --resource-group {rg} --name {name} --instance-id {id}', checks=[
258-
self.check('statuses[0].code',
259-
'ProvisioningState/succeeded'),
260-
self.check('statuses[1].code', 'PowerState/running'),
261-
])
262-
break
263-
except JMESPathCheckAssertionError:
264-
time.sleep(30)
292+
result = self.createVMSS()
293+
print("Sending SysRq...")
294+
stopwatch_start = time.time()
265295
self.cmd(
266296
'serial-console send sysrq -g {rg} -n {name} --instance-id {id} --input h')
297+
stopwatch_end = time.time()
298+
print(f"SysRq sent in {stopwatch_end - stopwatch_start} seconds.")
267299
self.check_result(resource_group, name,
268300
vmss_instanceid=result[1], message="sysrq: HELP")
269301

@@ -278,27 +310,13 @@ def test_send_nmi_VMSS(self, resource_group, storage_account):
278310
'urn': 'Ubuntu2204',
279311
'loc': 'westus2'
280312
})
281-
self.cmd(
282-
'az vmss create -g {rg} -n {name} --image {urn} --instance-count 2 -l {loc} --orchestration-mode uniform')
283-
self.cmd('az vmss update --name {name} --resource-group {rg} --set virtualMachineProfile.diagnosticsProfile="{{\\"bootDiagnostics\\": {{\\"Enabled\\" : \\"True\\",\\"StorageUri\\":\\"https://{sa}.blob.core.windows.net/\\"}}}}"')
284-
result = self.cmd(
285-
'vmss list-instances --resource-group {rg} --name {name} --query "[].instanceId"').get_output_in_json()
286-
self.kwargs.update({'id': result[1]})
287-
self.cmd(
288-
'az vmss update-instances -g {rg} -n {name} --instance-ids {id}')
289-
time.sleep(60)
290-
for i in range(5):
291-
try:
292-
self.cmd('vmss get-instance-view --resource-group {rg} --name {name} --instance-id {id}', checks=[
293-
self.check('statuses[0].code',
294-
'ProvisioningState/succeeded'),
295-
self.check('statuses[1].code', 'PowerState/running'),
296-
])
297-
break
298-
except JMESPathCheckAssertionError:
299-
time.sleep(30)
313+
result = self.createVMSS()
314+
print("Sending NMI...")
315+
stopwatch_start = time.time()
300316
self.cmd(
301317
'serial-console send nmi -g {rg} -n {name} --instance-id {id}')
318+
stopwatch_end = time.time()
319+
print(f"NMI sent in {stopwatch_end - stopwatch_start} seconds.")
302320
self.check_result(resource_group, name,
303321
vmss_instanceid=result[1], message="NMI received")
304322

@@ -313,26 +331,14 @@ def test_send_reset_VMSS(self, resource_group, storage_account):
313331
'urn': 'Ubuntu2204',
314332
'loc': 'westus2'
315333
})
316-
self.cmd(
317-
'az vmss create -g {rg} -n {name} --image {urn} --instance-count 2 -l {loc} --orchestration-mode uniform')
318-
self.cmd('az vmss update --name {name} --resource-group {rg} --set virtualMachineProfile.diagnosticsProfile="{{\\"bootDiagnostics\\": {{\\"Enabled\\" : \\"True\\",\\"StorageUri\\":\\"https://{sa}.blob.core.windows.net/\\"}}}}"')
319-
result = self.cmd(
320-
'vmss list-instances --resource-group {rg} --name {name} --query "[].instanceId"').get_output_in_json()
321-
self.kwargs.update({'id': result[1]})
322-
self.cmd(
323-
'az vmss update-instances -g {rg} -n {name} --instance-ids {id}')
324-
time.sleep(60)
325-
for i in range(5):
326-
try:
327-
self.cmd('vmss get-instance-view --resource-group {rg} --name {name} --instance-id {id}', checks=[
328-
self.check('statuses[0].code',
329-
'ProvisioningState/succeeded'),
330-
self.check('statuses[1].code', 'PowerState/running'),
331-
])
332-
break
333-
except JMESPathCheckAssertionError:
334-
time.sleep(30)
334+
result = self.createVMSS()
335+
print("Sending Reset...")
336+
stopwatch_start = time.time()
335337
self.cmd('serial-console send reset -g {rg} -n {name} --instance-id {id}')
338+
stopwatch_end = time.time()
339+
print(f"Reset sent in {stopwatch_end - stopwatch_start} seconds.")
340+
self.check_result(resource_group, name,
341+
vmss_instanceid=result[1], message="Record successful boot")
336342

337343
@ResourceGroupPreparer(name_prefix='cli_test_serialconsole', location='westus2')
338344
@StorageAccountPreparer(name_prefix='cli', location="westus2")
@@ -345,21 +351,12 @@ def test_send_nmi_VM(self, resource_group, storage_account):
345351
'urn': 'Ubuntu2204',
346352
'loc': 'westus2'
347353
})
348-
self.cmd(
349-
'az vm create -g {rg} -n {name} --image {urn} --boot-diagnostics-storage {sa} -l {loc} --generate-ssh-keys')
350-
time.sleep(60)
351-
for i in range(5):
352-
try:
353-
self.cmd('vm get-instance-view --resource-group {rg} --name {name}', checks=[
354-
self.check(
355-
'instanceView.statuses[0].code', 'ProvisioningState/succeeded'),
356-
self.check(
357-
'instanceView.statuses[1].code', 'PowerState/running'),
358-
])
359-
break
360-
except JMESPathCheckAssertionError:
361-
time.sleep(30)
354+
self.createVM()
355+
print("Sending NMI...")
356+
stopwatch_start = time.time()
362357
self.cmd('serial-console send nmi -g {rg} -n {name}')
358+
stopwatch_end = time.time()
359+
print(f"NMI sent in {stopwatch_end - stopwatch_start} seconds.")
363360
self.check_result(resource_group, name, message="NMI received")
364361

365362
@ResourceGroupPreparer(name_prefix='cli_test_serialconsole', location='westus2')
@@ -373,21 +370,12 @@ def test_send_sysrq_VM(self, resource_group, storage_account):
373370
'urn': 'Ubuntu2204',
374371
'loc': 'westus2'
375372
})
376-
self.cmd(
377-
'az vm create -g {rg} -n {name} --image {urn} --boot-diagnostics-storage {sa} -l {loc} --generate-ssh-keys')
378-
time.sleep(60)
379-
for i in range(5):
380-
try:
381-
self.cmd('vm get-instance-view --resource-group {rg} --name {name}', checks=[
382-
self.check(
383-
'instanceView.statuses[0].code', 'ProvisioningState/succeeded'),
384-
self.check(
385-
'instanceView.statuses[1].code', 'PowerState/running'),
386-
])
387-
break
388-
except JMESPathCheckAssertionError:
389-
time.sleep(30)
373+
self.createVM()
374+
print("Sending Sysrq...")
375+
stopwatch_start = time.time()
390376
self.cmd('serial-console send sysrq -g {rg} -n {name} --input h')
377+
stopwatch_end = time.time()
378+
print(f"Sysrq sent in {stopwatch_end - stopwatch_start} seconds.")
391379
self.check_result(resource_group, name, message="sysrq: HELP")
392380

393381
@ResourceGroupPreparer(name_prefix='cli_test_serialconsole', location='westus2')
@@ -401,6 +389,38 @@ def test_send_reset_VM(self, resource_group, storage_account):
401389
'urn': 'Ubuntu2204',
402390
'loc': 'westus2'
403391
})
392+
self.createVM()
393+
print("Sending Reset...")
394+
stopwatch_start = time.time()
395+
self.cmd('serial-console send reset -g {rg} -n {name}')
396+
stopwatch_end = time.time()
397+
print(f"Reset sent in {stopwatch_end - stopwatch_start} seconds.")
398+
self.check_result(resource_group, name, message="Record successful boot")
399+
400+
def createVMSS(self):
401+
self.cmd(
402+
'az vmss create -g {rg} -n {name} --image {urn} --instance-count 2 -l {loc} --orchestration-mode uniform')
403+
self.cmd(
404+
'az vmss update --name {name} --resource-group {rg} --set virtualMachineProfile.diagnosticsProfile="{{\\"bootDiagnostics\\": {{\\"Enabled\\" : \\"True\\",\\"StorageUri\\":\\"https://{sa}.blob.core.windows.net/\\"}}}}"')
405+
result = self.cmd(
406+
'vmss list-instances --resource-group {rg} --name {name} --query "[].instanceId"').get_output_in_json()
407+
self.kwargs.update({'id': result[1]})
408+
self.cmd(
409+
'az vmss update-instances -g {rg} -n {name} --instance-ids {id}')
410+
time.sleep(60)
411+
for i in range(5):
412+
try:
413+
self.cmd('vmss get-instance-view --resource-group {rg} --name {name} --instance-id {id}', checks=[
414+
self.check('statuses[0].code',
415+
'ProvisioningState/succeeded'),
416+
self.check('statuses[1].code', 'PowerState/running'),
417+
])
418+
break
419+
except JMESPathCheckAssertionError:
420+
time.sleep(30)
421+
return result
422+
423+
def createVM(self):
404424
self.cmd(
405425
'az vm create -g {rg} -n {name} --image {urn} --boot-diagnostics-storage {sa} -l {loc} --generate-ssh-keys')
406426
time.sleep(60)
@@ -415,4 +435,3 @@ def test_send_reset_VM(self, resource_group, storage_account):
415435
break
416436
except JMESPathCheckAssertionError:
417437
time.sleep(30)
418-
self.cmd('serial-console send reset -g {rg} -n {name}')

src/serial-console/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
# TODO: Confirm this is the right version number you want and it matches your
1818
# HISTORY.rst entry.
19-
VERSION = '1.0.0b2'
19+
VERSION = '1.0.0b3'
2020

2121
# The full list of classifiers is available at
2222
# https://pypi.python.org/pypi?%3Aaction=list_classifiers

0 commit comments

Comments
 (0)