Skip to content

Commit 3031d79

Browse files
committed
[AppService] az webapp status: Add new command to show per-instance Site Runtime Status
1 parent b3b6906 commit 3031d79

8 files changed

Lines changed: 1138 additions & 8 deletions

File tree

src/azure-cli/HISTORY.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ Release History
134134
* Fix #33180: `az functionapp plan create`: Simplify reserved parameter assignment in AppServicePlan (#33202)
135135
* `az webapp sitecontainers convert`: Add support for converting Docker Compose multi-container apps to Sitecontainers mode (#33131)
136136
* `az webapp up/deploy`: Add `--enriched-errors` parameter to see detailed deployment failure log (#32940)
137+
* `az webapp status`: Add new command to show per-instance Site Runtime Status (#160)
137138
* `az webapp create`: Add error message that clearly lists all valid options and specifies how to discover available runtimes (#33252)
138139
* `az appservice plan create`: Make `P0V3` as default SKU when `--sku` is omitted for linux webapp (#33237)
139140
* `az appservice plan create`: Add `PREMIUM0V3` tier for elastic scale (#33237)

src/azure-cli/azure/cli/command_modules/appservice/_help.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2478,6 +2478,20 @@
24782478
crafted: true
24792479
"""
24802480

2481+
helps['webapp status'] = """
2482+
type: command
2483+
short-summary: Show per-instance Site Runtime Status for a web app.
2484+
long-summary: >
2485+
Returns the runtime status of each instance.
2486+
examples:
2487+
- name: Show runtime status for all instances in the production slot.
2488+
text: az webapp status --name MyWebapp --resource-group MyResourceGroup
2489+
- name: Show runtime status for a deployment slot.
2490+
text: az webapp status --name MyWebapp --resource-group MyResourceGroup --slot staging
2491+
- name: Show runtime status for a specific instance.
2492+
text: az webapp status --name MyWebapp --resource-group MyResourceGroup --instance 6d3f0a2b8e5c4d1fb97a3c6e2f4a1b09
2493+
"""
2494+
24812495
helps['webapp ssh'] = """
24822496
type: command
24832497
short-summary: SSH command establishes a ssh session to the web container and developer would get a shell terminal remotely.

src/azure-cli/azure/cli/command_modules/appservice/_params.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ def load_arguments(self, _):
9494
help="the name of the slot. Default to the productions slot if not specified")
9595
c.argument('name', arg_type=webapp_name_arg_type)
9696

97+
with self.argument_context('webapp status') as c:
98+
c.argument('instance', options_list=['--instance'],
99+
help='show runtime status for a specific instance only. '
100+
"Run 'az webapp list-instances' to discover instance IDs.")
101+
97102
with self.argument_context('functionapp') as c:
98103
c.ignore('app_instance')
99104
c.argument('resource_group_name', arg_type=resource_group_name_type)

src/azure-cli/azure/cli/command_modules/appservice/commands.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ def transform_runtime_list_output(result):
4848
]) for r in result]
4949

5050

51+
def transform_webapp_status_output(result):
52+
from .custom import format_webapp_status_output
53+
return format_webapp_status_output(result)
54+
55+
5156
def ex_handler_factory(creating_plan=False):
5257
def _ex_handler(ex):
5358
ex = _polish_bad_errors(ex, creating_plan)
@@ -142,6 +147,7 @@ def load_command_table(self, _):
142147
g.custom_command('restart', 'restart_webapp')
143148
g.custom_command('browse', 'view_in_browser')
144149
g.custom_command('list-instances', 'list_instances')
150+
g.custom_command('status', 'show_webapp_status', table_transformer=transform_webapp_status_output)
145151
g.custom_command('list-runtimes', 'list_runtimes', table_transformer=transform_runtime_list_output)
146152
g.custom_command('identity assign', 'assign_identity')
147153
g.custom_show_command('identity show', 'show_identity')

src/azure-cli/azure/cli/command_modules/appservice/custom.py

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -385,10 +385,12 @@ def create_webapp(cmd, resource_group_name, name, plan, runtime=None, startup_fi
385385

386386
_enable_basic_auth(cmd, name, None, resource_group_name, basic_auth.lower())
387387
# Only suggest deployment command when no deployment method is already configured
388-
if not using_webapp_up and not any([container_image_name, deployment_container_image_name,
389-
multicontainer_config_type, sitecontainers_app,
390-
deployment_source_url, deployment_local_git]):
391-
logger.warning("Webapp '%s' created. Deploy your code with: az webapp deploy", name)
388+
if not using_webapp_up:
389+
if not any([container_image_name, deployment_container_image_name,
390+
multicontainer_config_type, sitecontainers_app,
391+
deployment_source_url, deployment_local_git]):
392+
logger.warning("Webapp '%s' created. Deploy your code with: az webapp deploy", name)
393+
_log_webapp_status_tip(name, resource_group_name)
392394
return webapp
393395

394396

@@ -2460,6 +2462,85 @@ def show_app(cmd, resource_group_name, name, slot=None):
24602462
return app
24612463

24622464

2465+
def _log_webapp_status_tip(name, resource_group_name):
2466+
logger.warning("Tip: run 'az webapp status --name %s --resource-group %s' "
2467+
"to see per-instance runtime status.",
2468+
name, resource_group_name)
2469+
2470+
2471+
def _extract_webapp_status_items(result):
2472+
# The siteStatus ARM API returns a ResponseMessageEnvelope whose 'properties'
2473+
# holds the list of per-instance SiteRuntimeStatusOnWorker objects.
2474+
if isinstance(result, dict):
2475+
properties = result.get('properties')
2476+
if isinstance(properties, list):
2477+
return properties
2478+
if isinstance(properties, dict):
2479+
return [properties]
2480+
return []
2481+
2482+
2483+
def format_webapp_status_output(result):
2484+
from collections import OrderedDict
2485+
2486+
items = _extract_webapp_status_items(result)
2487+
# LastError is a nullable field on the backend SiteRuntimeStatusOnWorker contract,
2488+
# so the error columns (LastError, LastErrorDetails, LastErrorTimestamp) are only
2489+
# surfaced when at least one instance reports a LastError. Details and DetailsLevel
2490+
# are always serialized by the backend and are always shown.
2491+
show_errors = any(item.get('lastError') for item in items)
2492+
2493+
rows = []
2494+
for item in items:
2495+
row = OrderedDict([
2496+
('InstanceId', item.get('instanceId')),
2497+
('State', item.get('state')),
2498+
('Action', item.get('action'))
2499+
])
2500+
if show_errors:
2501+
row['LastError'] = item.get('lastError')
2502+
row['LastErrorDetails'] = item.get('lastErrorDetails')
2503+
row['LastErrorTimestamp'] = item.get('lastErrorTimestamp')
2504+
row['Details'] = item.get('details')
2505+
row['DetailsLevel'] = item.get('detailsLevel')
2506+
rows.append(row)
2507+
return rows
2508+
2509+
2510+
def show_webapp_status(cmd, resource_group_name, name, slot=None, instance=None):
2511+
from azure.cli.core.commands.client_factory import get_subscription_id
2512+
2513+
client = web_client_factory(cmd.cli_ctx)
2514+
subscription_id = get_subscription_id(cmd.cli_ctx)
2515+
api_version = client.DEFAULT_API_VERSION
2516+
slot_segment = '/slots/{}'.format(slot) if slot else ''
2517+
# When an instance is requested, call the dedicated siteStatus/{instanceId} route
2518+
# so the backend resolves and validates the instance (returning 404 if missing).
2519+
instance_segment = '/{}'.format(instance) if instance else ''
2520+
request_url = (
2521+
'{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/sites/{}{}/siteStatus{}'
2522+
'?api-version={}'.format(
2523+
cmd.cli_ctx.cloud.endpoints.resource_manager,
2524+
subscription_id,
2525+
resource_group_name,
2526+
name,
2527+
slot_segment,
2528+
instance_segment,
2529+
api_version
2530+
)
2531+
)
2532+
2533+
try:
2534+
return send_raw_request(cmd.cli_ctx, 'GET', request_url).json()
2535+
except HttpResponseError as ex:
2536+
if instance and ex.status_code == 404:
2537+
scope = 'webapp and slot' if slot else 'webapp'
2538+
raise ResourceNotFoundError(
2539+
"Instance '{}' was not found for this {}. "
2540+
"Run 'az webapp list-instances' to see available instance IDs.".format(instance, scope))
2541+
raise
2542+
2543+
24632544
def _list_app(cli_ctx, resource_group_name=None, show_details=False):
24642545
client = web_client_factory(cli_ctx)
24652546
if resource_group_name:
@@ -3583,11 +3664,15 @@ def stop_webapp(cmd, resource_group_name, name, slot=None):
35833664

35843665

35853666
def start_webapp(cmd, resource_group_name, name, slot=None):
3586-
return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'start', slot)
3667+
result = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'start', slot)
3668+
_log_webapp_status_tip(name, resource_group_name)
3669+
return result
35873670

35883671

35893672
def restart_webapp(cmd, resource_group_name, name, slot=None):
3590-
return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'restart', slot)
3673+
result = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'restart', slot)
3674+
_log_webapp_status_tip(name, resource_group_name)
3675+
return result
35913676

35923677

35933678
def get_site_configs(cmd, resource_group_name, name, slot=None):
@@ -9931,6 +10016,7 @@ def _poll_deployment_runtime_status(cmd, resource_group_name, webapp_name, slot,
993110016
time_elapsed = 0
993210017
deployment_status = None
993310018
response_body = None
10019+
status_tip_logged = False
993410020
while time_elapsed < max_time_sec:
993510021
try:
993610022
response_body = send_raw_request(cmd.cli_ctx, "GET", deploymentstatusapi_url).json()
@@ -9945,10 +10031,15 @@ def _poll_deployment_runtime_status(cmd, resource_group_name, webapp_name, slot,
994510031
status = deployment_status if status is None else status
994610032
logger.warning("Status: %s Time: %s(s)", status, time_elapsed)
994710033
if deployment_status == "RuntimeStarting":
10034+
if not status_tip_logged:
10035+
_log_webapp_status_tip(webapp_name, resource_group_name)
10036+
status_tip_logged = True
994810037
logger.info("InprogressInstances: %s, SuccessfulInstances: %s",
994910038
deployment_properties.get('numberOfInstancesInProgress'),
995010039
deployment_properties.get('numberOfInstancesSuccessful'))
995110040
if deployment_status == "RuntimeSuccessful":
10041+
if not status_tip_logged:
10042+
_log_webapp_status_tip(webapp_name, resource_group_name)
995210043
break
995310044
if deployment_status == "RuntimeFailed":
995410045
error_text = ""
@@ -10834,6 +10925,8 @@ def webapp_up(cmd, name=None, resource_group_name=None, plan=None, location=None
1083410925
logger.warning("You can launch the app at %s", _url)
1083510926
create_json.update({'URL': _url})
1083610927

10928+
_log_webapp_status_tip(name, rg_name)
10929+
1083710930
if logs:
1083810931
_configure_default_logging(cmd, rg_name, name)
1083910932
try:
@@ -11588,6 +11681,8 @@ def _make_onedeploy_request(params):
1158811681
logger.warning("Deployment status is: \"%s\"", state)
1158911682
response_body = response.json().get("properties", {})
1159011683
logger.warning("Deployment has completed successfully")
11684+
if not (poll_async_deployment_for_debugging and params.track_status):
11685+
_log_webapp_status_tip(params.webapp_name, params.resource_group_name)
1159111686
logger.warning("You can visit your app at: %s", _get_visit_url(params))
1159211687
return response_body
1159311688

0 commit comments

Comments
 (0)