diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_help.py b/src/azure-cli/azure/cli/command_modules/appservice/_help.py index e0dad92e98c..57c39a7e06d 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -1489,7 +1489,14 @@ helps['webapp config appsettings set'] = """ type: command short-summary: Set a web app's settings. -long-summary: Note that setting values are now redacted in the result. Please use the `az webapp config appsettings list` command to view the settings. +long-summary: | + Note that setting values are now redacted in the result. Please use the `az webapp config appsettings list` command to view the settings. + + To enable Application Insights monitoring, set the APPLICATIONINSIGHTS_CONNECTION_STRING app setting to the + connection string of your Application Insights resource. You can find the connection string in the Azure Portal + under your Application Insights resource's Overview page. Using the connection string is the recommended approach + over the legacy APPINSIGHTS_INSTRUMENTATIONKEY setting. For more information, see + https://learn.microsoft.com/azure/azure-monitor/app/azure-web-apps parameters: - name: --settings short-summary: Space-separated appsettings in KEY=VALUE format. Use @{file} to load from a file. See https://go.microsoft.com/fwlink/?linkid=2219923 for more information on file format and editing app settings in bulk. @@ -1502,6 +1509,12 @@ - name: Set using both key-value pair and a json file with more settings. text: > az webapp config appsettings set -g MyResourceGroup -n MyUniqueApp --settings mySetting=value @moreSettings.json + - name: Enable Application Insights monitoring using a connection string (recommended). + text: > + az webapp config appsettings set -g MyResourceGroup -n MyUniqueApp --settings APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://region.in.applicationinsights.azure.com/" + - name: Enable Application Insights monitoring using an instrumentation key (legacy). + text: > + az webapp config appsettings set -g MyResourceGroup -n MyUniqueApp --settings APPINSIGHTS_INSTRUMENTATIONKEY=00000000-0000-0000-0000-000000000000 """ helps['webapp config backup'] = """ @@ -1512,10 +1525,22 @@ helps['webapp config backup create'] = """ type: command short-summary: Create a backup of a web app. +long-summary: | + Create a backup of a web app to an Azure Blob Storage container. + The --container-url must be a SAS URL with write permissions to the target container. + + VNet / Private Endpoint note: If the storage account is behind a VNet or private endpoint, + the web app must have VNet integration enabled and the storage account's firewall must allow + access from the web app's outbound VNet. Ensure the SAS token is valid and the app's + outbound traffic can reach the storage account. For more information, see + https://learn.microsoft.com/azure/app-service/web-sites-backup#troubleshooting examples: - - name: Create a backup of a web app. (autogenerated) + - name: Create a backup of a web app. text: az webapp config backup create --container-url {container-url} --resource-group MyResourceGroup --webapp-name MyWebapp crafted: true + - name: Create a backup with a custom name and database. + text: > + az webapp config backup create --container-url {container-url} --resource-group MyResourceGroup --webapp-name MyWebapp --backup-name mybackup --db-name mydb --db-type SqlAzure --db-connection-string "Server=..." """ helps['webapp config backup list'] = """ @@ -1530,6 +1555,21 @@ helps['webapp config backup restore'] = """ type: command short-summary: Restore a web app from a backup. +long-summary: | + Restore a web app from a previously created backup stored in Azure Blob Storage. + + VNet / Private Endpoint note: If the storage account is behind a VNet or private endpoint, + the web app must have VNet integration enabled and the storage account's firewall must allow + access from the web app's outbound VNet. Ensure the SAS token is valid and the app's + outbound traffic can reach the storage account. For more information, see + https://learn.microsoft.com/azure/app-service/web-sites-backup#troubleshooting +examples: + - name: Restore a web app from a backup. + text: > + az webapp config backup restore --container-url {container-url} --resource-group MyResourceGroup --webapp-name MyWebapp --backup-name mybackup + - name: Restore a web app overwriting the existing app. + text: > + az webapp config backup restore --container-url {container-url} --resource-group MyResourceGroup --webapp-name MyWebapp --backup-name mybackup --overwrite """ helps['webapp config backup delete'] = """ @@ -2285,13 +2325,29 @@ helps['webapp log config'] = """ type: command short-summary: Configure logging for a web app. -examples: - - name: Configure logging for a web app. (autogenerated) +long-summary: | + Configure application-level and web server logging. Web server logs can be stored on the filesystem + or in Azure Blob Storage. When using Azure Blob Storage, provide a SAS URL via --web-server-log-sas-url. +parameters: + - name: --web-server-logging + short-summary: "Configure Web server logging. Use 'filesystem' for local storage, 'azureblobstorage' for Azure Blob Storage, or 'off' to disable." + - name: --web-server-log-sas-url + short-summary: SAS URL to an Azure Blob Storage container for web server log storage. Required when --web-server-logging is set to azureblobstorage. + - name: --web-server-log-retention + short-summary: Number of days to retain web server logs when using Azure Blob Storage (default 3). +examples: + - name: Disable web server logging. text: az webapp log config --name MyWebapp --resource-group MyResourceGroup --web-server-logging off crafted: true - - name: Configure logging for a web app. (autogenerated) + - name: Disable Docker container logging. text: az webapp log config --docker-container-logging off --name MyWebapp --resource-group MyResourceGroup crafted: true + - name: Enable web server logging to Azure Blob Storage. + text: > + az webapp log config --name MyWebapp --resource-group MyResourceGroup --web-server-logging azureblobstorage --web-server-log-sas-url "https://mystorageaccount.blob.core.windows.net/weblogs?sv=...&sig=..." + - name: Enable application logging to the filesystem at verbose level. + text: > + az webapp log config --name MyWebapp --resource-group MyResourceGroup --application-logging filesystem --level verbose """ helps['webapp log download'] = """ @@ -2431,10 +2487,16 @@ helps['webapp traffic-routing set'] = """ type: command short-summary: Configure routing traffic to deployment slots. +long-summary: | + Configure the percentage of production traffic routed to deployment slots. + Note: this command updates the site configuration, which may cause a brief app restart on the platform. + This is a known Azure App Service platform behavior when modifying ramp-up rules via the configuration API. examples: - - name: Configure routing traffic to deployment slots. (autogenerated) + - name: Route 50% of production traffic to the staging slot. text: az webapp traffic-routing set --distribution staging=50 --name MyWebApp --resource-group MyResourceGroup crafted: true + - name: Split traffic across multiple slots. + text: az webapp traffic-routing set --distribution staging=30 canary=20 --name MyWebApp --resource-group MyResourceGroup """ helps['webapp traffic-routing show'] = """ diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_params.py b/src/azure-cli/azure/cli/command_modules/appservice/_params.py index bd4dc0b7ea5..05428948a20 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -750,9 +750,13 @@ def load_arguments(self, _): c.argument('level', help='logging level', arg_type=get_enum_type(['error', 'warning', 'information', 'verbose'])) c.argument('web_server_logging', help='configure Web server logging', - arg_type=get_enum_type(['off', 'filesystem'])) + arg_type=get_enum_type(['off', 'filesystem', 'azureblobstorage'])) c.argument('docker_container_logging', help='configure gathering STDOUT and STDERR output from container', arg_type=get_enum_type(['off', 'filesystem'])) + c.argument('web_server_log_sas_url', options_list=['--web-server-log-sas-url'], + help='SAS URL to an Azure Blob Storage container for web server log storage. Required when --web-server-logging is set to azureblobstorage.') + c.argument('web_server_log_retention', type=int, options_list=['--web-server-log-retention'], + help='Number of days to retain web server logs when using Azure Blob Storage. Default: 3.') with self.argument_context('webapp log tail') as c: c.argument('provider', diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 386ba088608..34daa356365 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -5464,10 +5464,12 @@ def _get_url(cmd, resource_group_name, name, slot=None): def config_diagnostics(cmd, resource_group_name, name, level=None, application_logging=None, web_server_logging=None, docker_container_logging=None, detailed_error_messages=None, - failed_request_tracing=None, slot=None): + failed_request_tracing=None, slot=None, + web_server_log_sas_url=None, web_server_log_retention=None): from azure.mgmt.web.models import (FileSystemApplicationLogsConfig, ApplicationLogsConfig, AzureBlobStorageApplicationLogsConfig, SiteLogsConfig, HttpLogsConfig, FileSystemHttpLogsConfig, + AzureBlobStorageHttpLogsConfig, EnabledConfig) client = web_client_factory(cmd.cli_ctx) # TODO: ensure we call get_site only once @@ -5492,15 +5494,23 @@ def config_diagnostics(cmd, resource_group_name, name, level=None, http_logs = None server_logging_option = web_server_logging or docker_container_logging if server_logging_option: - # TODO: az blob storage log config currently not in use, will be impelemented later. - # Tracked as Issue: #4764 on Github filesystem_log_config = None + blob_log_config = None turned_on = server_logging_option != 'off' if server_logging_option in ['filesystem', 'off']: # 100 mb max log size, retention lasts 3 days. Yes we hard code it, portal does too filesystem_log_config = FileSystemHttpLogsConfig(retention_in_mb=100, retention_in_days=3, enabled=turned_on) - http_logs = HttpLogsConfig(file_system=filesystem_log_config, azure_blob_storage=None) + if server_logging_option == 'azureblobstorage': + if not web_server_log_sas_url: + raise RequiredArgumentMissingError( + '--web-server-log-sas-url is required when --web-server-logging is set to azureblobstorage.') + retention = web_server_log_retention if web_server_log_retention is not None else 3 + blob_log_config = AzureBlobStorageHttpLogsConfig( + sas_url=web_server_log_sas_url, + retention_in_days=retention, + enabled=True) + http_logs = HttpLogsConfig(file_system=filesystem_log_config, azure_blob_storage=blob_log_config) detailed_error_messages_logs = (None if detailed_error_messages is None else EnabledConfig(enabled=detailed_error_messages)) @@ -5622,6 +5632,8 @@ def set_traffic_routing(cmd, resource_group_name, name, distribution): site = client.web_apps.get(resource_group_name, name) if not site: raise ResourceNotFoundError("'{}' app doesn't exist".format(name)) + logger.warning('Traffic routing updates the site configuration, which may cause a brief restart. ' + 'This is a known platform behavior.') configs = get_site_configs(cmd, resource_group_name, name) host_name_split = site.default_host_name.split('.', 1) host_name_suffix = '.' + host_name_split[1] diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index 853eadc1edd..f493897e0cf 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -12,7 +12,8 @@ from knack.util import CLIError from azure.cli.core.azclierror import (InvalidArgumentValueError, MutuallyExclusiveArgumentError, - AzureResponseError) + AzureResponseError, + RequiredArgumentMissingError) from azure.cli.command_modules.appservice.custom import (set_deployment_user, update_git_token, add_hostname, update_site_configs, @@ -33,7 +34,9 @@ add_github_actions, update_app_settings, update_application_settings_polling, - update_webapp) + update_webapp, + config_diagnostics, + set_traffic_routing) # pylint: disable=line-too-long from azure.cli.core.profiles import ResourceType @@ -603,6 +606,68 @@ def test_update_app_settings_success_with_slot_settings(self, mock_build, mock_s mock_client.web_apps.update_slot_configuration_names.assert_called_once() mock_build.assert_called_once() + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory') + def test_config_diagnostics_blob_storage_requires_sas_url(self, mock_client_factory): + """Test that RequiredArgumentMissingError is raised when azureblobstorage is set without SAS URL.""" + cmd_mock = _get_test_cmd() + + mock_client = mock.MagicMock() + mock_client.web_apps.get.return_value = mock.MagicMock() + mock_client_factory.return_value = mock_client + + with self.assertRaisesRegex(RequiredArgumentMissingError, + '--web-server-log-sas-url is required'): + config_diagnostics(cmd_mock, 'test-rg', 'test-app', + web_server_logging='azureblobstorage') + + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation') + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory') + def test_config_diagnostics_blob_storage_with_sas_url(self, mock_client_factory, mock_site_op): + """Test that blob storage logging is configured when SAS URL is provided.""" + cmd_mock = _get_test_cmd() + + mock_client = mock.MagicMock() + mock_client.web_apps.get.return_value = mock.MagicMock() + mock_client_factory.return_value = mock_client + mock_site_op.return_value = mock.MagicMock() + + sas_url = 'https://mystorageaccount.blob.core.windows.net/logs?sv=2021-06-08&sig=abc' + config_diagnostics(cmd_mock, 'test-rg', 'test-app', + web_server_logging='azureblobstorage', + web_server_log_sas_url=sas_url, + web_server_log_retention=7) + + mock_site_op.assert_called_once() + call_args = mock_site_op.call_args + site_log_config = call_args[0][5] + self.assertIsNotNone(site_log_config.http_logs) + self.assertIsNotNone(site_log_config.http_logs.azure_blob_storage) + self.assertEqual(site_log_config.http_logs.azure_blob_storage.sas_url, sas_url) + self.assertEqual(site_log_config.http_logs.azure_blob_storage.retention_in_days, 7) + self.assertTrue(site_log_config.http_logs.azure_blob_storage.enabled) + + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation') + @mock.patch('azure.cli.command_modules.appservice.custom.get_site_configs') + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory') + def test_set_traffic_routing_warns_about_restart(self, mock_client_factory, mock_get_configs, mock_site_op): + """Test that set_traffic_routing emits a warning about potential restart.""" + cmd_mock = _get_test_cmd() + + mock_site = mock.MagicMock() + mock_site.default_host_name = 'myapp.azurewebsites.net' + mock_client = mock.MagicMock() + mock_client.web_apps.get.return_value = mock_site + mock_client_factory.return_value = mock_client + + mock_configs = mock.MagicMock() + mock_configs.experiments.ramp_up_rules = [] + mock_get_configs.return_value = mock_configs + + import logging + with self.assertLogs('cli.azure.cli.command_modules.appservice.custom', level=logging.WARNING) as log: + set_traffic_routing(cmd_mock, 'test-rg', 'myapp', ['staging=50']) + self.assertTrue(any('restart' in msg.lower() for msg in log.output)) + class TestUpdateWebapp(unittest.TestCase):