Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions src/azure-cli/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ Release History
* Fix #33180: `az functionapp plan create`: Simplify reserved parameter assignment in AppServicePlan (#33202)
* `az webapp sitecontainers convert`: Add support for converting Docker Compose multi-container apps to Sitecontainers mode (#33131)
* `az webapp up/deploy`: Add `--enriched-errors` parameter to see detailed deployment failure log (#32940)
* `az webapp status`: Add new command to show per-instance Site Runtime Status (#33632)
* `az webapp create`: Add error message that clearly lists all valid options and specifies how to discover available runtimes (#33252)
* `az appservice plan create`: Make `P0V3` as default SKU when `--sku` is omitted for linux webapp (#33237)
* `az appservice plan create`: Add `PREMIUM0V3` tier for elastic scale (#33237)
Expand Down
14 changes: 14 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appservice/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -2478,6 +2478,20 @@
crafted: true
"""

helps['webapp status'] = """
type: command
short-summary: Show per-instance Site Runtime Status for a web app.
long-summary: >
Returns the runtime status of each instance.
examples:
- name: Show runtime status for all instances in the production slot.
text: az webapp status --name MyWebapp --resource-group MyResourceGroup
- name: Show runtime status for a deployment slot.
text: az webapp status --name MyWebapp --resource-group MyResourceGroup --slot staging
- name: Show runtime status for a specific instance.
text: az webapp status --name MyWebapp --resource-group MyResourceGroup --instance 6d3f0a2b8e5c4d1fb97a3c6e2f4a1b09
"""

helps['webapp ssh'] = """
type: command
short-summary: SSH command establishes a ssh session to the web container and developer would get a shell terminal remotely.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ def load_arguments(self, _):
help="the name of the slot. Default to the productions slot if not specified")
c.argument('name', arg_type=webapp_name_arg_type)

with self.argument_context('webapp status') as c:
c.argument('instance', options_list=['--instance'],
help='show runtime status for a specific instance only. '
"Run 'az webapp list-instances' to discover instance IDs.")

with self.argument_context('functionapp') as c:
c.ignore('app_instance')
c.argument('resource_group_name', arg_type=resource_group_name_type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ def transform_runtime_list_output(result):
]) for r in result]


def transform_webapp_status_output(result):
from .custom import format_webapp_status_output
return format_webapp_status_output(result)


def ex_handler_factory(creating_plan=False):
def _ex_handler(ex):
ex = _polish_bad_errors(ex, creating_plan)
Expand Down Expand Up @@ -142,6 +147,7 @@ def load_command_table(self, _):
g.custom_command('restart', 'restart_webapp')
g.custom_command('browse', 'view_in_browser')
g.custom_command('list-instances', 'list_instances')
g.custom_command('status', 'show_webapp_status', table_transformer=transform_webapp_status_output)
g.custom_command('list-runtimes', 'list_runtimes', table_transformer=transform_runtime_list_output)
g.custom_command('identity assign', 'assign_identity')
g.custom_show_command('identity show', 'show_identity')
Expand Down
96 changes: 92 additions & 4 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,10 +385,12 @@ def create_webapp(cmd, resource_group_name, name, plan, runtime=None, startup_fi

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


Expand Down Expand Up @@ -2460,6 +2462,80 @@ def show_app(cmd, resource_group_name, name, slot=None):
return app


def _log_webapp_status_tip(name, resource_group_name, is_linux):
# Per-instance runtime status (siteStatus) is a Linux App Service feature,
# so only surface the tip for Linux webapps.
if not is_linux:
return
logger.warning("Tip: run 'az webapp status --name %s --resource-group %s' "
"to see per-instance runtime status.",
name, resource_group_name)


def _extract_webapp_status_items(result):
# The siteStatus response holds per-instance status under 'properties':
# a list for /siteStatus, a single object for /siteStatus/{instanceId}.
# Normalize both shapes into a list for uniform formatting.
properties = result.get('properties')
if isinstance(properties, list):
return properties
if isinstance(properties, dict):
return [properties]
return []


def format_webapp_status_output(result):
from collections import OrderedDict

items = _extract_webapp_status_items(result)
# LastError is a nullable field on the backend SiteRuntimeStatusOnWorker contract,
# so the error columns (LastError, LastErrorDetails, LastErrorTimestamp) are only
# surfaced when at least one instance reports a LastError.
Comment thread
HaripriyaMehta marked this conversation as resolved.
show_errors = any(item.get('lastError') for item in items)

rows = []
for item in items:
row = OrderedDict([
('InstanceId', item.get('instanceId')),
('State', item.get('state')),
('Action', item.get('action'))
])
if show_errors:
row['LastError'] = item.get('lastError')
row['LastErrorDetails'] = item.get('lastErrorDetails')
row['LastErrorTimestamp'] = item.get('lastErrorTimestamp')
row['Details'] = item.get('details')
row['DetailsLevel'] = item.get('detailsLevel')
rows.append(row)
return rows


def show_webapp_status(cmd, resource_group_name, name, slot=None, instance=None):
Comment thread
HaripriyaMehta marked this conversation as resolved.
from azure.cli.core.commands.client_factory import get_subscription_id

client = web_client_factory(cmd.cli_ctx)
subscription_id = get_subscription_id(cmd.cli_ctx)
api_version = client.DEFAULT_API_VERSION
slot_segment = f'/slots/{slot}' if slot else ''
instance_segment = f'/{instance}' if instance else ''
base_url = (
f'/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}'
f'/providers/Microsoft.Web/sites/{name}{slot_segment}/siteStatus{instance_segment}'
f'?api-version={api_version}'
)
request_url = cmd.cli_ctx.cloud.endpoints.resource_manager + base_url

try:
return send_raw_request(cmd.cli_ctx, 'GET', request_url).json()
except HttpResponseError as ex:
if instance and ex.status_code == 404:
scope = 'webapp and slot' if slot else 'webapp'
raise ResourceNotFoundError(
f"Instance '{instance}' was not found for this {scope}. "
"Run 'az webapp list-instances' to see available instance IDs.")
raise


def _list_app(cli_ctx, resource_group_name=None, show_details=False):
client = web_client_factory(cli_ctx)
if resource_group_name:
Expand Down Expand Up @@ -9931,6 +10007,7 @@ def _poll_deployment_runtime_status(cmd, resource_group_name, webapp_name, slot,
time_elapsed = 0
deployment_status = None
response_body = None
status_tip_logged = False
while time_elapsed < max_time_sec:
try:
response_body = send_raw_request(cmd.cli_ctx, "GET", deploymentstatusapi_url).json()
Expand All @@ -9945,12 +10022,19 @@ def _poll_deployment_runtime_status(cmd, resource_group_name, webapp_name, slot,
status = deployment_status if status is None else status
logger.warning("Status: %s Time: %s(s)", status, time_elapsed)
if deployment_status == "RuntimeStarting":
if not status_tip_logged:
_log_webapp_status_tip(webapp_name, resource_group_name, True)
status_tip_logged = True
logger.info("InprogressInstances: %s, SuccessfulInstances: %s",
deployment_properties.get('numberOfInstancesInProgress'),
deployment_properties.get('numberOfInstancesSuccessful'))
if deployment_status == "RuntimeSuccessful":
if not status_tip_logged:
_log_webapp_status_tip(webapp_name, resource_group_name, True)
break
if deployment_status == "RuntimeFailed":
Comment thread
HaripriyaMehta marked this conversation as resolved.
if not status_tip_logged:
_log_webapp_status_tip(webapp_name, resource_group_name, True)
error_text = ""
total_num_instances = int(deployment_properties.get('numberOfInstancesInProgress')) + \
int(deployment_properties.get('numberOfInstancesSuccessful')) + \
Expand Down Expand Up @@ -10834,6 +10918,8 @@ def webapp_up(cmd, name=None, resource_group_name=None, plan=None, location=None
logger.warning("You can launch the app at %s", _url)
create_json.update({'URL': _url})

_log_webapp_status_tip(name, rg_name, _is_linux)

if logs:
_configure_default_logging(cmd, rg_name, name)
try:
Expand Down Expand Up @@ -11588,6 +11674,8 @@ def _make_onedeploy_request(params):
logger.warning("Deployment status is: \"%s\"", state)
response_body = response.json().get("properties", {})
logger.warning("Deployment has completed successfully")
if not (poll_async_deployment_for_debugging and params.track_status):
_log_webapp_status_tip(params.webapp_name, params.resource_group_name, params.is_linux_webapp)
logger.warning("You can visit your app at: %s", _get_visit_url(params))
return response_body

Expand Down
Loading
Loading