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 6import threading 7import time 8import ast 9 10try: 11 from urllib.parse import urlparse 12except ImportError: 13 from urlparse import urlparse # pylint: disable=import-error 14from binascii import hexlify 15from os import urandom 16import datetime 17import json 18import ssl 19import sys 20import uuid 21from functools import reduce 22from nacl import encoding, public 23 24from six.moves.urllib.request import urlopen # pylint: disable=import-error, ungrouped-imports 25import OpenSSL.crypto 26from fabric import Connection 27 28from knack.prompting import prompt_pass, NoTTYException, prompt_y_n 29from knack.util import CLIError 30from knack.log import get_logger 31 32from msrestazure.azure_exceptions import CloudError 33from msrestazure.tools import is_valid_resource_id, parse_resource_id, resource_id 34 35from azure.mgmt.storage import StorageManagementClient 36from azure.mgmt.applicationinsights import ApplicationInsightsManagementClient 37from azure.mgmt.relay.models import AccessRights 38from azure.mgmt.web.models import KeyInfo 39from azure.cli.command_modules.relay._client_factory import hycos_mgmt_client_factory, namespaces_mgmt_client_factory 40from azure.cli.command_modules.network._client_factory import network_client_factory 41 42from azure.cli.core.commands.client_factory import get_mgmt_service_client 43from azure.cli.core.commands import LongRunningOperation 44from azure.cli.core.util import in_cloud_console, shell_safe_json_parse, open_page_in_browser, get_json_object, \ 45 ConfiguredDefaultSetter, sdk_no_wait, get_file_json 46from azure.cli.core.util import get_az_user_agent, send_raw_request 47from azure.cli.core.profiles import ResourceType, get_sdk 48from azure.cli.core.azclierror import (ResourceNotFoundError, RequiredArgumentMissingError, ValidationError, 49 CLIInternalError, UnclassifiedUserFault, AzureResponseError, 50 ArgumentUsageError, MutuallyExclusiveArgumentError) 51 52from .tunnel import TunnelServer 53 54from ._params import AUTH_TYPES, MULTI_CONTAINER_TYPES 55from ._client_factory import web_client_factory, ex_handler_factory, providers_client_factory 56from ._appservice_utils import _generic_site_operation, _generic_settings_operation 57from .utils import (_normalize_sku, 58 get_sku_name, 59 retryable_method, 60 raise_missing_token_suggestion, 61 _get_location_from_resource_group, 62 _list_app, 63 _rename_server_farm_props, 64 _get_location_from_webapp) 65from ._create_util import (zip_contents_from_dir, get_runtime_version_details, create_resource_group, get_app_details, 66 check_resource_group_exists, set_location, get_site_availability, get_profile_username, 67 get_plan_to_use, get_lang_from_content, get_rg_to_use, get_sku_to_use, 68 detect_os_form_src, get_current_stack_from_runtime, generate_default_app_name) 69from ._constants import (FUNCTIONS_STACKS_API_JSON_PATHS, FUNCTIONS_STACKS_API_KEYS, 70 FUNCTIONS_LINUX_RUNTIME_VERSION_REGEX, FUNCTIONS_WINDOWS_RUNTIME_VERSION_REGEX, 71 NODE_EXACT_VERSION_DEFAULT, RUNTIME_STACKS, FUNCTIONS_NO_V2_REGIONS, PUBLIC_CLOUD, 72 LINUX_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH, WINDOWS_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH) 73from ._github_oauth import (get_github_access_token) 74 75logger = get_logger(__name__) 76 77# pylint:disable=no-member,too-many-lines,too-many-locals 78 79# region "Common routines shared with quick-start extensions." 80# Please maintain compatibility in both interfaces and functionalities" 81 82 83def create_webapp(cmd, resource_group_name, name, plan, runtime=None, startup_file=None, # pylint: disable=too-many-statements,too-many-branches 84 deployment_container_image_name=None, deployment_source_url=None, deployment_source_branch='master', 85 deployment_local_git=None, docker_registry_server_password=None, docker_registry_server_user=None, 86 multicontainer_config_type=None, multicontainer_config_file=None, tags=None, 87 using_webapp_up=False, language=None, assign_identities=None, 88 role='Contributor', scope=None): 89 SiteConfig, SkuDescription, Site, NameValuePair = cmd.get_models( 90 'SiteConfig', 'SkuDescription', 'Site', 'NameValuePair') 91 if deployment_source_url and deployment_local_git: 92 raise CLIError('usage error: --deployment-source-url <url> | --deployment-local-git') 93 94 docker_registry_server_url = parse_docker_image_name(deployment_container_image_name) 95 96 client = web_client_factory(cmd.cli_ctx) 97 if is_valid_resource_id(plan): 98 parse_result = parse_resource_id(plan) 99 plan_info = client.app_service_plans.get(parse_result['resource_group'], parse_result['name']) 100 else: 101 plan_info = client.app_service_plans.get(name=plan, resource_group_name=resource_group_name) 102 if not plan_info: 103 raise CLIError("The plan '{}' doesn't exist in the resource group '{}".format(plan, resource_group_name)) 104 is_linux = plan_info.reserved 105 node_default_version = NODE_EXACT_VERSION_DEFAULT 106 location = plan_info.location 107 # This is to keep the existing appsettings for a newly created webapp on existing webapp name. 108 name_validation = get_site_availability(cmd, name) 109 if not name_validation.name_available: 110 if name_validation.reason == 'Invalid': 111 raise CLIError(name_validation.message) 112 logger.warning("Webapp '%s' already exists. The command will use the existing app's settings.", name) 113 app_details = get_app_details(cmd, name) 114 if app_details is None: 115 raise CLIError("Unable to retrieve details of the existing app '{}'. Please check that " 116 "the app is a part of the current subscription".format(name)) 117 current_rg = app_details.resource_group 118 if resource_group_name is not None and (resource_group_name.lower() != current_rg.lower()): 119 raise CLIError("The webapp '{}' exists in resource group '{}' and does not " 120 "match the value entered '{}'. Please re-run command with the " 121 "correct parameters.". format(name, current_rg, resource_group_name)) 122 existing_app_settings = _generic_site_operation(cmd.cli_ctx, resource_group_name, 123 name, 'list_application_settings') 124 settings = [] 125 for k, v in existing_app_settings.properties.items(): 126 settings.append(NameValuePair(name=k, value=v)) 127 site_config = SiteConfig(app_settings=settings) 128 else: 129 site_config = SiteConfig(app_settings=[]) 130 if isinstance(plan_info.sku, SkuDescription) and plan_info.sku.name.upper() not in ['F1', 'FREE', 'SHARED', 'D1', 131 'B1', 'B2', 'B3', 'BASIC']: 132 site_config.always_on = True 133 webapp_def = Site(location=location, site_config=site_config, server_farm_id=plan_info.id, tags=tags, 134 https_only=using_webapp_up) 135 helper = _StackRuntimeHelper(cmd, client, linux=is_linux) 136 if runtime: 137 runtime = helper.remove_delimiters(runtime) 138 139 current_stack = None 140 if is_linux: 141 if not validate_container_app_create_options(runtime, deployment_container_image_name, 142 multicontainer_config_type, multicontainer_config_file): 143 raise CLIError("usage error: --runtime | --deployment-container-image-name |" 144 " --multicontainer-config-type TYPE --multicontainer-config-file FILE") 145 if startup_file: 146 site_config.app_command_line = startup_file 147 148 if runtime: 149 match = helper.resolve(runtime) 150 if not match: 151 raise CLIError("Linux Runtime '{}' is not supported." 152 " Please invoke 'az webapp list-runtimes --linux' to cross check".format(runtime)) 153 match['setter'](cmd=cmd, stack=match, site_config=site_config) 154 elif deployment_container_image_name: 155 site_config.linux_fx_version = _format_fx_version(deployment_container_image_name) 156 if name_validation.name_available: 157 site_config.app_settings.append(NameValuePair(name="WEBSITES_ENABLE_APP_SERVICE_STORAGE", 158 value="false")) 159 elif multicontainer_config_type and multicontainer_config_file: 160 encoded_config_file = _get_linux_multicontainer_encoded_config_from_file(multicontainer_config_file) 161 site_config.linux_fx_version = _format_fx_version(encoded_config_file, multicontainer_config_type) 162 163 elif plan_info.is_xenon: # windows container webapp 164 if deployment_container_image_name: 165 site_config.windows_fx_version = _format_fx_version(deployment_container_image_name) 166 # set the needed app settings for container image validation 167 if name_validation.name_available: 168 site_config.app_settings.append(NameValuePair(name="DOCKER_REGISTRY_SERVER_USERNAME", 169 value=docker_registry_server_user)) 170 site_config.app_settings.append(NameValuePair(name="DOCKER_REGISTRY_SERVER_PASSWORD", 171 value=docker_registry_server_password)) 172 site_config.app_settings.append(NameValuePair(name="DOCKER_REGISTRY_SERVER_URL", 173 value=docker_registry_server_url)) 174 175 elif runtime: # windows webapp with runtime specified 176 if any([startup_file, deployment_container_image_name, multicontainer_config_file, multicontainer_config_type]): 177 raise CLIError("usage error: --startup-file or --deployment-container-image-name or " 178 "--multicontainer-config-type and --multicontainer-config-file is " 179 "only appliable on linux webapp") 180 match = helper.resolve(runtime) 181 if not match: 182 raise CLIError("Windows runtime '{}' is not supported. " 183 "Please invoke 'az webapp list-runtimes' to cross check".format(runtime)) 184 match['setter'](cmd=cmd, stack=match, site_config=site_config) 185 186 # TODO: Ask Calvin the purpose of this - seems like unneeded set of calls 187 # portal uses the current_stack propety in metadata to display stack for windows apps 188 current_stack = get_current_stack_from_runtime(runtime) 189 190 else: # windows webapp without runtime specified 191 if name_validation.name_available: # If creating new webapp 192 site_config.app_settings.append(NameValuePair(name="WEBSITE_NODE_DEFAULT_VERSION", 193 value=node_default_version)) 194 195 if site_config.app_settings: 196 for setting in site_config.app_settings: 197 logger.info('Will set appsetting %s', setting) 198 if using_webapp_up: # when the routine is invoked as a help method for webapp up 199 if name_validation.name_available: 200 logger.info("will set appsetting for enabling build") 201 site_config.app_settings.append(NameValuePair(name="SCM_DO_BUILD_DURING_DEPLOYMENT", value=True)) 202 if language is not None and language.lower() == 'dotnetcore': 203 if name_validation.name_available: 204 site_config.app_settings.append(NameValuePair(name='ANCM_ADDITIONAL_ERROR_PAGE_LINK', 205 value='https://{}.scm.azurewebsites.net/detectors' 206 .format(name))) 207 208 poller = client.web_apps.begin_create_or_update(resource_group_name, name, webapp_def) 209 webapp = LongRunningOperation(cmd.cli_ctx)(poller) 210 211 # TO DO: (Check with Calvin) This seems to be something specific to portal client use only & should be removed 212 if current_stack: 213 _update_webapp_current_stack_property_if_needed(cmd, resource_group_name, name, current_stack) 214 215 # Ensure SCC operations follow right after the 'create', no precedent appsetting update commands 216 _set_remote_or_local_git(cmd, webapp, resource_group_name, name, deployment_source_url, 217 deployment_source_branch, deployment_local_git) 218 219 _fill_ftp_publishing_url(cmd, webapp, resource_group_name, name) 220 221 if deployment_container_image_name: 222 logger.info("Updating container settings") 223 update_container_settings(cmd, resource_group_name, name, docker_registry_server_url, 224 deployment_container_image_name, docker_registry_server_user, 225 docker_registry_server_password=docker_registry_server_password) 226 227 if assign_identities is not None: 228 identity = assign_identity(cmd, resource_group_name, name, assign_identities, 229 role, None, scope) 230 webapp.identity = identity 231 232 return webapp 233 234 235def validate_container_app_create_options(runtime=None, deployment_container_image_name=None, 236 multicontainer_config_type=None, multicontainer_config_file=None): 237 if bool(multicontainer_config_type) != bool(multicontainer_config_file): 238 return False 239 opts = [runtime, deployment_container_image_name, multicontainer_config_type] 240 return len([x for x in opts if x]) == 1 # you can only specify one out the combinations 241 242 243def parse_docker_image_name(deployment_container_image_name): 244 if not deployment_container_image_name: 245 return None 246 slash_ix = deployment_container_image_name.rfind('/') 247 docker_registry_server_url = deployment_container_image_name[0:slash_ix] 248 if slash_ix == -1 or ("." not in docker_registry_server_url and ":" not in docker_registry_server_url): 249 return None 250 return docker_registry_server_url 251 252 253def update_app_settings(cmd, resource_group_name, name, settings=None, slot=None, slot_settings=None): 254 if not settings and not slot_settings: 255 raise CLIError('Usage Error: --settings |--slot-settings') 256 257 settings = settings or [] 258 slot_settings = slot_settings or [] 259 260 app_settings = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 261 'list_application_settings', slot) 262 result, slot_result = {}, {} 263 # pylint: disable=too-many-nested-blocks 264 for src, dest, setting_type in [(settings, result, "Settings"), (slot_settings, slot_result, "SlotSettings")]: 265 for s in src: 266 try: 267 temp = shell_safe_json_parse(s) 268 if isinstance(temp, list): # a bit messy, but we'd like accept the output of the "list" command 269 for t in temp: 270 if 'slotSetting' in t.keys(): 271 slot_result[t['name']] = t['slotSetting'] 272 if setting_type == "SlotSettings": 273 slot_result[t['name']] = True 274 result[t['name']] = t['value'] 275 else: 276 dest.update(temp) 277 except CLIError: 278 setting_name, value = s.split('=', 1) 279 dest[setting_name] = value 280 result.update(dest) 281 282 for setting_name, value in result.items(): 283 app_settings.properties[setting_name] = value 284 client = web_client_factory(cmd.cli_ctx) 285 286 result = _generic_settings_operation(cmd.cli_ctx, resource_group_name, name, 287 'update_application_settings', 288 app_settings, slot, client) 289 290 app_settings_slot_cfg_names = [] 291 if slot_result: 292 slot_cfg_names = client.web_apps.list_slot_configuration_names(resource_group_name, name) 293 slot_cfg_names.app_setting_names = slot_cfg_names.app_setting_names or [] 294 # Slot settings logic to add a new setting(s) or remove an existing setting(s) 295 for slot_setting_name, value in slot_result.items(): 296 if value and slot_setting_name not in slot_cfg_names.app_setting_names: 297 slot_cfg_names.app_setting_names.append(slot_setting_name) 298 elif not value and slot_setting_name in slot_cfg_names.app_setting_names: 299 slot_cfg_names.app_setting_names.remove(slot_setting_name) 300 app_settings_slot_cfg_names = slot_cfg_names.app_setting_names 301 client.web_apps.update_slot_configuration_names(resource_group_name, name, slot_cfg_names) 302 303 return _build_app_settings_output(result.properties, app_settings_slot_cfg_names) 304 305 306def add_azure_storage_account(cmd, resource_group_name, name, custom_id, storage_type, account_name, 307 share_name, access_key, mount_path=None, slot=None, slot_setting=False): 308 AzureStorageInfoValue = cmd.get_models('AzureStorageInfoValue') 309 azure_storage_accounts = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 310 'list_azure_storage_accounts', slot) 311 312 if custom_id in azure_storage_accounts.properties: 313 raise CLIError("Site already configured with an Azure storage account with the id '{}'. " 314 "Use 'az webapp config storage-account update' to update an existing " 315 "Azure storage account configuration.".format(custom_id)) 316 317 azure_storage_accounts.properties[custom_id] = AzureStorageInfoValue(type=storage_type, account_name=account_name, 318 share_name=share_name, access_key=access_key, 319 mount_path=mount_path) 320 client = web_client_factory(cmd.cli_ctx) 321 322 result = _generic_settings_operation(cmd.cli_ctx, resource_group_name, name, 323 'update_azure_storage_accounts', azure_storage_accounts, 324 slot, client) 325 326 if slot_setting: 327 slot_cfg_names = client.web_apps.list_slot_configuration_names(resource_group_name, name) 328 329 slot_cfg_names.azure_storage_config_names = slot_cfg_names.azure_storage_config_names or [] 330 if custom_id not in slot_cfg_names.azure_storage_config_names: 331 slot_cfg_names.azure_storage_config_names.append(custom_id) 332 client.web_apps.update_slot_configuration_names(resource_group_name, name, slot_cfg_names) 333 334 return result.properties 335 336 337def update_azure_storage_account(cmd, resource_group_name, name, custom_id, storage_type=None, account_name=None, 338 share_name=None, access_key=None, mount_path=None, slot=None, slot_setting=False): 339 AzureStorageInfoValue = cmd.get_models('AzureStorageInfoValue') 340 341 azure_storage_accounts = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 342 'list_azure_storage_accounts', slot) 343 344 existing_account_config = azure_storage_accounts.properties.pop(custom_id, None) 345 346 if not existing_account_config: 347 raise CLIError("No Azure storage account configuration found with the id '{}'. " 348 "Use 'az webapp config storage-account add' to add a new " 349 "Azure storage account configuration.".format(custom_id)) 350 351 new_account_config = AzureStorageInfoValue( 352 type=storage_type or existing_account_config.type, 353 account_name=account_name or existing_account_config.account_name, 354 share_name=share_name or existing_account_config.share_name, 355 access_key=access_key or existing_account_config.access_key, 356 mount_path=mount_path or existing_account_config.mount_path 357 ) 358 359 azure_storage_accounts.properties[custom_id] = new_account_config 360 361 client = web_client_factory(cmd.cli_ctx) 362 result = _generic_settings_operation(cmd.cli_ctx, resource_group_name, name, 363 'update_azure_storage_accounts', azure_storage_accounts, 364 slot, client) 365 366 if slot_setting: 367 slot_cfg_names = client.web_apps.list_slot_configuration_names(resource_group_name, name) 368 slot_cfg_names.azure_storage_config_names = slot_cfg_names.azure_storage_config_names or [] 369 if custom_id not in slot_cfg_names.azure_storage_config_names: 370 slot_cfg_names.azure_storage_config_names.append(custom_id) 371 client.web_apps.update_slot_configuration_names(resource_group_name, name, slot_cfg_names) 372 373 return result.properties 374 375 376def enable_zip_deploy_functionapp(cmd, resource_group_name, name, src, build_remote=False, timeout=None, slot=None): 377 client = web_client_factory(cmd.cli_ctx) 378 app = client.web_apps.get(resource_group_name, name) 379 if app is None: 380 raise CLIError('The function app \'{}\' was not found in resource group \'{}\'. ' 381 'Please make sure these values are correct.'.format(name, resource_group_name)) 382 parse_plan_id = parse_resource_id(app.server_farm_id) 383 plan_info = None 384 retry_delay = 10 # seconds 385 # We need to retry getting the plan because sometimes if the plan is created as part of function app, 386 # it can take a couple of tries before it gets the plan 387 for _ in range(5): 388 plan_info = client.app_service_plans.get(parse_plan_id['resource_group'], 389 parse_plan_id['name']) 390 if plan_info is not None: 391 break 392 time.sleep(retry_delay) 393 394 if build_remote and not app.reserved: 395 raise CLIError('Remote build is only available on Linux function apps') 396 397 is_consumption = is_plan_consumption(cmd, plan_info) 398 if (not build_remote) and is_consumption and app.reserved: 399 return upload_zip_to_storage(cmd, resource_group_name, name, src, slot) 400 if build_remote: 401 add_remote_build_app_settings(cmd, resource_group_name, name, slot) 402 else: 403 remove_remote_build_app_settings(cmd, resource_group_name, name, slot) 404 405 return enable_zip_deploy(cmd, resource_group_name, name, src, timeout, slot) 406 407 408def enable_zip_deploy_webapp(cmd, resource_group_name, name, src, timeout=None, slot=None): 409 return enable_zip_deploy(cmd, resource_group_name, name, src, timeout=timeout, slot=slot) 410 411 412def enable_zip_deploy(cmd, resource_group_name, name, src, timeout=None, slot=None): 413 logger.warning("Getting scm site credentials for zip deployment") 414 user_name, password = _get_site_credential(cmd.cli_ctx, resource_group_name, name, slot) 415 416 try: 417 scm_url = _get_scm_url(cmd, resource_group_name, name, slot) 418 except ValueError: 419 raise CLIError('Failed to fetch scm url for function app') 420 421 zip_url = scm_url + '/api/zipdeploy?isAsync=true' 422 deployment_status_url = scm_url + '/api/deployments/latest' 423 424 import urllib3 425 authorization = urllib3.util.make_headers(basic_auth='{0}:{1}'.format(user_name, password)) 426 headers = authorization 427 headers['Content-Type'] = 'application/octet-stream' 428 headers['Cache-Control'] = 'no-cache' 429 headers['User-Agent'] = get_az_user_agent() 430 431 import requests 432 import os 433 from azure.cli.core.util import should_disable_connection_verify 434 # Read file content 435 with open(os.path.realpath(os.path.expanduser(src)), 'rb') as fs: 436 zip_content = fs.read() 437 logger.warning("Starting zip deployment. This operation can take a while to complete ...") 438 res = requests.post(zip_url, data=zip_content, headers=headers, verify=not should_disable_connection_verify()) 439 logger.warning("Deployment endpoint responded with status code %d", res.status_code) 440 441 # check if there's an ongoing process 442 if res.status_code == 409: 443 raise CLIError("There may be an ongoing deployment or your app setting has WEBSITE_RUN_FROM_PACKAGE. " 444 "Please track your deployment in {} and ensure the WEBSITE_RUN_FROM_PACKAGE app setting " 445 "is removed. Use 'az webapp config appsettings list --name MyWebapp --resource-group " 446 "MyResourceGroup --subscription MySubscription' to list app settings and 'az webapp " 447 "config appsettings delete --name MyWebApp --resource-group MyResourceGroup " 448 "--setting-names <setting-names> to delete them.".format(deployment_status_url)) 449 450 # check the status of async deployment 451 response = _check_zip_deployment_status(cmd, resource_group_name, name, deployment_status_url, 452 authorization, timeout) 453 return response 454 455 456def add_remote_build_app_settings(cmd, resource_group_name, name, slot): 457 settings = get_app_settings(cmd, resource_group_name, name, slot) 458 scm_do_build_during_deployment = None 459 website_run_from_package = None 460 enable_oryx_build = None 461 462 app_settings_should_not_have = [] 463 app_settings_should_contain = {} 464 465 for keyval in settings: 466 value = keyval['value'].lower() 467 if keyval['name'] == 'SCM_DO_BUILD_DURING_DEPLOYMENT': 468 scm_do_build_during_deployment = value in ('true', '1') 469 if keyval['name'] == 'WEBSITE_RUN_FROM_PACKAGE': 470 website_run_from_package = value 471 if keyval['name'] == 'ENABLE_ORYX_BUILD': 472 enable_oryx_build = value 473 474 if scm_do_build_during_deployment is not True: 475 logger.warning("Setting SCM_DO_BUILD_DURING_DEPLOYMENT to true") 476 update_app_settings(cmd, resource_group_name, name, [ 477 "SCM_DO_BUILD_DURING_DEPLOYMENT=true" 478 ], slot) 479 app_settings_should_contain['SCM_DO_BUILD_DURING_DEPLOYMENT'] = 'true' 480 481 if website_run_from_package: 482 logger.warning("Removing WEBSITE_RUN_FROM_PACKAGE app setting") 483 delete_app_settings(cmd, resource_group_name, name, [ 484 "WEBSITE_RUN_FROM_PACKAGE" 485 ], slot) 486 app_settings_should_not_have.append('WEBSITE_RUN_FROM_PACKAGE') 487 488 if enable_oryx_build: 489 logger.warning("Removing ENABLE_ORYX_BUILD app setting") 490 delete_app_settings(cmd, resource_group_name, name, [ 491 "ENABLE_ORYX_BUILD" 492 ], slot) 493 app_settings_should_not_have.append('ENABLE_ORYX_BUILD') 494 495 # Wait for scm site to get the latest app settings 496 if app_settings_should_not_have or app_settings_should_contain: 497 logger.warning("Waiting SCM site to be updated with the latest app settings") 498 scm_is_up_to_date = False 499 retries = 10 500 while not scm_is_up_to_date and retries >= 0: 501 scm_is_up_to_date = validate_app_settings_in_scm( 502 cmd, resource_group_name, name, slot, 503 should_contain=app_settings_should_contain, 504 should_not_have=app_settings_should_not_have) 505 retries -= 1 506 time.sleep(5) 507 508 if retries < 0: 509 logger.warning("App settings may not be propagated to the SCM site.") 510 511 512def remove_remote_build_app_settings(cmd, resource_group_name, name, slot): 513 settings = get_app_settings(cmd, resource_group_name, name, slot) 514 scm_do_build_during_deployment = None 515 516 app_settings_should_contain = {} 517 518 for keyval in settings: 519 value = keyval['value'].lower() 520 if keyval['name'] == 'SCM_DO_BUILD_DURING_DEPLOYMENT': 521 scm_do_build_during_deployment = value in ('true', '1') 522 523 if scm_do_build_during_deployment is not False: 524 logger.warning("Setting SCM_DO_BUILD_DURING_DEPLOYMENT to false") 525 update_app_settings(cmd, resource_group_name, name, [ 526 "SCM_DO_BUILD_DURING_DEPLOYMENT=false" 527 ], slot) 528 app_settings_should_contain['SCM_DO_BUILD_DURING_DEPLOYMENT'] = 'false' 529 530 # Wait for scm site to get the latest app settings 531 if app_settings_should_contain: 532 logger.warning("Waiting SCM site to be updated with the latest app settings") 533 scm_is_up_to_date = False 534 retries = 10 535 while not scm_is_up_to_date and retries >= 0: 536 scm_is_up_to_date = validate_app_settings_in_scm( 537 cmd, resource_group_name, name, slot, 538 should_contain=app_settings_should_contain) 539 retries -= 1 540 time.sleep(5) 541 542 if retries < 0: 543 logger.warning("App settings may not be propagated to the SCM site") 544 545 546def upload_zip_to_storage(cmd, resource_group_name, name, src, slot=None): 547 settings = get_app_settings(cmd, resource_group_name, name, slot) 548 549 storage_connection = None 550 for keyval in settings: 551 if keyval['name'] == 'AzureWebJobsStorage': 552 storage_connection = str(keyval['value']) 553 554 if storage_connection is None: 555 raise CLIError('Could not find a \'AzureWebJobsStorage\' application setting') 556 557 container_name = "function-releases" 558 blob_name = "{}-{}.zip".format(datetime.datetime.today().strftime('%Y%m%d%H%M%S'), str(uuid.uuid4())) 559 BlockBlobService = get_sdk(cmd.cli_ctx, ResourceType.DATA_STORAGE, 'blob#BlockBlobService') 560 block_blob_service = BlockBlobService(connection_string=storage_connection) 561 if not block_blob_service.exists(container_name): 562 block_blob_service.create_container(container_name) 563 564 # https://gist.github.com/vladignatyev/06860ec2040cb497f0f3 565 def progress_callback(current, total): 566 total_length = 30 567 filled_length = int(round(total_length * current) / float(total)) 568 percents = round(100.0 * current / float(total), 1) 569 progress_bar = '=' * filled_length + '-' * (total_length - filled_length) 570 progress_message = 'Uploading {} {}%'.format(progress_bar, percents) 571 cmd.cli_ctx.get_progress_controller().add(message=progress_message) 572 573 block_blob_service.create_blob_from_path(container_name, blob_name, src, validate_content=True, 574 progress_callback=progress_callback) 575 576 now = datetime.datetime.utcnow() 577 blob_start = now - datetime.timedelta(minutes=10) 578 blob_end = now + datetime.timedelta(weeks=520) 579 BlobPermissions = get_sdk(cmd.cli_ctx, ResourceType.DATA_STORAGE, 'blob#BlobPermissions') 580 blob_token = block_blob_service.generate_blob_shared_access_signature(container_name, 581 blob_name, 582 permission=BlobPermissions(read=True), 583 expiry=blob_end, 584 start=blob_start) 585 586 blob_uri = block_blob_service.make_blob_url(container_name, blob_name, sas_token=blob_token) 587 website_run_from_setting = "WEBSITE_RUN_FROM_PACKAGE={}".format(blob_uri) 588 update_app_settings(cmd, resource_group_name, name, settings=[website_run_from_setting]) 589 client = web_client_factory(cmd.cli_ctx) 590 591 try: 592 logger.info('\nSyncing Triggers...') 593 if slot is not None: 594 client.web_apps.sync_function_triggers_slot(resource_group_name, name, slot) 595 else: 596 client.web_apps.sync_function_triggers(resource_group_name, name) 597 except CloudError as ex: 598 # This SDK function throws an error if Status Code is 200 599 if ex.status_code != 200: 600 raise ex 601 except Exception as ex: # pylint: disable=broad-except 602 if ex.response.status_code != 200: 603 raise ex 604 605 606def show_webapp(cmd, resource_group_name, name, slot=None): 607 return _show_app(cmd, resource_group_name, name, "webapp", slot) 608 609 610# for generic updater 611def get_webapp(cmd, resource_group_name, name, slot=None): 612 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot) 613 614 615def set_webapp(cmd, resource_group_name, name, slot=None, skip_dns_registration=None, # pylint: disable=unused-argument 616 skip_custom_domain_verification=None, force_dns_registration=None, ttl_in_seconds=None, **kwargs): # pylint: disable=unused-argument 617 instance = kwargs['parameters'] 618 client = web_client_factory(cmd.cli_ctx) 619 updater = client.web_apps.begin_create_or_update_slot if slot else client.web_apps.begin_create_or_update 620 kwargs = dict(resource_group_name=resource_group_name, name=name, site_envelope=instance) 621 if slot: 622 kwargs['slot'] = slot 623 624 return updater(**kwargs) 625 626 627def update_webapp(instance, client_affinity_enabled=None, https_only=None): 628 if 'function' in instance.kind: 629 raise CLIError("please use 'az functionapp update' to update this function app") 630 if client_affinity_enabled is not None: 631 instance.client_affinity_enabled = client_affinity_enabled == 'true' 632 if https_only is not None: 633 instance.https_only = https_only == 'true' 634 635 return instance 636 637 638def update_functionapp(cmd, instance, plan=None, force=False): 639 client = web_client_factory(cmd.cli_ctx) 640 if plan is not None: 641 if is_valid_resource_id(plan): 642 dest_parse_result = parse_resource_id(plan) 643 dest_plan_info = client.app_service_plans.get(dest_parse_result['resource_group'], 644 dest_parse_result['name']) 645 else: 646 dest_plan_info = client.app_service_plans.get(instance.resource_group, plan) 647 if dest_plan_info is None: 648 raise ResourceNotFoundError("The plan '{}' doesn't exist".format(plan)) 649 validate_plan_switch_compatibility(cmd, client, instance, dest_plan_info, force) 650 instance.server_farm_id = dest_plan_info.id 651 return instance 652 653 654def validate_plan_switch_compatibility(cmd, client, src_functionapp_instance, dest_plan_instance, force): 655 general_switch_msg = 'Currently the switch is only allowed between a Consumption or an Elastic Premium plan.' 656 src_parse_result = parse_resource_id(src_functionapp_instance.server_farm_id) 657 src_plan_info = client.app_service_plans.get(src_parse_result['resource_group'], 658 src_parse_result['name']) 659 660 if src_plan_info is None: 661 raise ResourceNotFoundError('Could not determine the current plan of the functionapp') 662 663 # Ensure all plans involved are windows. Reserved = true indicates Linux. 664 if src_plan_info.reserved or dest_plan_instance.reserved: 665 raise ValidationError('This feature currently supports windows to windows plan migrations. For other ' 666 'migrations, please redeploy.') 667 668 src_is_premium = is_plan_elastic_premium(cmd, src_plan_info) 669 dest_is_consumption = is_plan_consumption(cmd, dest_plan_instance) 670 671 if not (is_plan_consumption(cmd, src_plan_info) or src_is_premium): 672 raise ValidationError('Your functionapp is not using a Consumption or an Elastic Premium plan. ' + 673 general_switch_msg) 674 if not (dest_is_consumption or is_plan_elastic_premium(cmd, dest_plan_instance)): 675 raise ValidationError('You are trying to move to a plan that is not a Consumption or an ' 676 'Elastic Premium plan. ' + 677 general_switch_msg) 678 679 if src_is_premium and dest_is_consumption: 680 logger.warning('WARNING: Moving a functionapp from Premium to Consumption might result in loss of ' 681 'functionality and cause the app to break. Please ensure the functionapp is compatible ' 682 'with a Consumption plan and is not using any features only available in Premium.') 683 if not force: 684 raise RequiredArgumentMissingError('If you want to migrate a functionapp from a Premium to Consumption ' 685 'plan, please re-run this command with the \'--force\' flag.') 686 687 688def set_functionapp(cmd, resource_group_name, name, **kwargs): 689 instance = kwargs['parameters'] 690 client = web_client_factory(cmd.cli_ctx) 691 return client.web_apps.begin_create_or_update(resource_group_name, name, site_envelope=instance) 692 693 694def get_functionapp(cmd, resource_group_name, name, slot=None): 695 function_app = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot) 696 if not function_app or 'function' not in function_app.kind: 697 raise ResourceNotFoundError("Unable to find App {} in resource group {}".format(name, resource_group_name)) 698 return function_app 699 700 701def show_functionapp(cmd, resource_group_name, name, slot=None): 702 return _show_app(cmd, resource_group_name, name, 'functionapp', slot) 703 704 705def list_webapp(cmd, resource_group_name=None): 706 full_list = _list_app(cmd.cli_ctx, resource_group_name) 707 # ignore apps with kind==null & not functions apps 708 return list(filter(lambda x: x.kind is not None and "function" not in x.kind.lower(), full_list)) 709 710 711def list_deleted_webapp(cmd, resource_group_name=None, name=None, slot=None): 712 result = _list_deleted_app(cmd.cli_ctx, resource_group_name, name, slot) 713 return sorted(result, key=lambda site: site.deleted_site_id) 714 715 716def restore_deleted_webapp(cmd, deleted_id, resource_group_name, name, slot=None, restore_content_only=None): 717 DeletedAppRestoreRequest = cmd.get_models('DeletedAppRestoreRequest') 718 request = DeletedAppRestoreRequest(deleted_site_id=deleted_id, recover_configuration=not restore_content_only) 719 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'begin_restore_from_deleted_app', 720 slot, request) 721 722 723def list_function_app(cmd, resource_group_name=None): 724 return list(filter(lambda x: x.kind is not None and "function" in x.kind.lower(), 725 _list_app(cmd.cli_ctx, resource_group_name))) 726 727 728def _show_app(cmd, resource_group_name, name, cmd_app_type, slot=None): 729 app = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot) 730 if not app: 731 raise ResourceNotFoundError("Unable to find {} '{}', in RG '{}'.".format( 732 cmd_app_type, name, resource_group_name)) 733 app_type = _kind_to_app_type(app.kind) if app else None 734 if app_type != cmd_app_type: 735 raise ResourceNotFoundError( 736 "Unable to find {} '{}', in RG '{}'".format(cmd_app_type.value, name, resource_group_name), 737 "Use 'az {} show' to show {}s".format(app_type.value, app_type.value)) 738 app.site_config = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get_configuration', slot) 739 _rename_server_farm_props(app) 740 _fill_ftp_publishing_url(cmd, app, resource_group_name, name, slot) 741 return app 742 743 744def _kind_to_app_type(kind): 745 if "workflow" in kind: 746 return "logicapp" 747 if "function" in kind: 748 return "functionapp" 749 return "webapp" 750 751 752def _list_app(cli_ctx, resource_group_name=None): 753 client = web_client_factory(cli_ctx) 754 if resource_group_name: 755 result = list(client.web_apps.list_by_resource_group(resource_group_name)) 756 else: 757 result = list(client.web_apps.list()) 758 for webapp in result: 759 _rename_server_farm_props(webapp) 760 return result 761 762 763def _list_deleted_app(cli_ctx, resource_group_name=None, name=None, slot=None): 764 client = web_client_factory(cli_ctx) 765 locations = _get_deleted_apps_locations(cli_ctx) 766 result = list() 767 for location in locations: 768 result = result + list(client.deleted_web_apps.list_by_location(location)) 769 if resource_group_name: 770 result = [r for r in result if r.resource_group == resource_group_name] 771 if name: 772 result = [r for r in result if r.deleted_site_name.lower() == name.lower()] 773 if slot: 774 result = [r for r in result if r.slot.lower() == slot.lower()] 775 return result 776 777 778def _build_identities_info(identities): 779 from ._appservice_utils import MSI_LOCAL_ID 780 identities = identities or [] 781 identity_types = [] 782 if not identities or MSI_LOCAL_ID in identities: 783 identity_types.append('SystemAssigned') 784 external_identities = [x for x in identities if x != MSI_LOCAL_ID] 785 if external_identities: 786 identity_types.append('UserAssigned') 787 identity_types = ','.join(identity_types) 788 info = {'type': identity_types} 789 if external_identities: 790 info['userAssignedIdentities'] = {e: {} for e in external_identities} 791 return (info, identity_types, external_identities, 'SystemAssigned' in identity_types) 792 793 794def assign_identity(cmd, resource_group_name, name, assign_identities=None, role='Contributor', slot=None, scope=None): 795 ManagedServiceIdentity, ResourceIdentityType = cmd.get_models('ManagedServiceIdentity', 796 'ManagedServiceIdentityType') 797 UserAssignedIdentitiesValue = cmd.get_models('Components1Jq1T4ISchemasManagedserviceidentityPropertiesUserassignedidentitiesAdditionalproperties') # pylint: disable=line-too-long 798 _, _, external_identities, enable_local_identity = _build_identities_info(assign_identities) 799 800 def getter(): 801 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot) 802 803 def setter(webapp): 804 if webapp.identity and webapp.identity.type == ResourceIdentityType.system_assigned_user_assigned: 805 identity_types = ResourceIdentityType.system_assigned_user_assigned 806 elif webapp.identity and webapp.identity.type == ResourceIdentityType.system_assigned and external_identities: 807 identity_types = ResourceIdentityType.system_assigned_user_assigned 808 elif webapp.identity and webapp.identity.type == ResourceIdentityType.user_assigned and enable_local_identity: 809 identity_types = ResourceIdentityType.system_assigned_user_assigned 810 elif external_identities and enable_local_identity: 811 identity_types = ResourceIdentityType.system_assigned_user_assigned 812 elif external_identities: 813 identity_types = ResourceIdentityType.user_assigned 814 else: 815 identity_types = ResourceIdentityType.system_assigned 816 817 if webapp.identity: 818 webapp.identity.type = identity_types 819 else: 820 webapp.identity = ManagedServiceIdentity(type=identity_types) 821 if external_identities: 822 if not webapp.identity.user_assigned_identities: 823 webapp.identity.user_assigned_identities = {} 824 for identity in external_identities: 825 webapp.identity.user_assigned_identities[identity] = UserAssignedIdentitiesValue() 826 827 poller = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'begin_create_or_update', 828 extra_parameter=webapp, slot=slot) 829 return LongRunningOperation(cmd.cli_ctx)(poller) 830 831 from azure.cli.core.commands.arm import assign_identity as _assign_identity 832 webapp = _assign_identity(cmd.cli_ctx, getter, setter, role, scope) 833 return webapp.identity 834 835 836def show_identity(cmd, resource_group_name, name, slot=None): 837 web_app = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot) 838 if not web_app: 839 raise ResourceNotFoundError("Unable to find App {} in resource group {}".format(name, resource_group_name)) 840 return web_app.identity 841 842 843def remove_identity(cmd, resource_group_name, name, remove_identities=None, slot=None): 844 IdentityType = cmd.get_models('ManagedServiceIdentityType') 845 UserAssignedIdentitiesValue = cmd.get_models('Components1Jq1T4ISchemasManagedserviceidentityPropertiesUserassignedidentitiesAdditionalproperties') # pylint: disable=line-too-long 846 _, _, external_identities, remove_local_identity = _build_identities_info(remove_identities) 847 848 def getter(): 849 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot) 850 851 def setter(webapp): 852 if webapp.identity is None: 853 return webapp 854 to_remove = [] 855 existing_identities = {x.lower() for x in list((webapp.identity.user_assigned_identities or {}).keys())} 856 if external_identities: 857 to_remove = {x.lower() for x in external_identities} 858 non_existing = to_remove.difference(existing_identities) 859 if non_existing: 860 raise CLIError("'{}' are not associated with '{}'".format(','.join(non_existing), name)) 861 if not list(existing_identities - to_remove): 862 if webapp.identity.type == IdentityType.user_assigned: 863 webapp.identity.type = IdentityType.none 864 elif webapp.identity.type == IdentityType.system_assigned_user_assigned: 865 webapp.identity.type = IdentityType.system_assigned 866 867 webapp.identity.user_assigned_identities = None 868 if remove_local_identity: 869 webapp.identity.type = (IdentityType.none 870 if webapp.identity.type == IdentityType.system_assigned or 871 webapp.identity.type == IdentityType.none 872 else IdentityType.user_assigned) 873 874 if webapp.identity.type not in [IdentityType.none, IdentityType.system_assigned]: 875 webapp.identity.user_assigned_identities = {} 876 if to_remove: 877 for identity in list(existing_identities - to_remove): 878 webapp.identity.user_assigned_identities[identity] = UserAssignedIdentitiesValue() 879 else: 880 for identity in list(existing_identities): 881 webapp.identity.user_assigned_identities[identity] = UserAssignedIdentitiesValue() 882 883 poller = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'begin_create_or_update', slot, webapp) 884 return LongRunningOperation(cmd.cli_ctx)(poller) 885 886 from azure.cli.core.commands.arm import assign_identity as _assign_identity 887 webapp = _assign_identity(cmd.cli_ctx, getter, setter) 888 return webapp.identity 889 890 891def get_auth_settings(cmd, resource_group_name, name, slot=None): 892 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get_auth_settings', slot) 893 894 895def is_auth_runtime_version_valid(runtime_version=None): 896 if runtime_version is None: 897 return True 898 if runtime_version.startswith("~") and len(runtime_version) > 1: 899 try: 900 int(runtime_version[1:]) 901 except ValueError: 902 return False 903 return True 904 split_versions = runtime_version.split('.') 905 if len(split_versions) != 3: 906 return False 907 for version in split_versions: 908 try: 909 int(version) 910 except ValueError: 911 return False 912 return True 913 914 915def update_auth_settings(cmd, resource_group_name, name, enabled=None, action=None, # pylint: disable=unused-argument 916 client_id=None, token_store_enabled=None, runtime_version=None, # pylint: disable=unused-argument 917 token_refresh_extension_hours=None, # pylint: disable=unused-argument 918 allowed_external_redirect_urls=None, client_secret=None, # pylint: disable=unused-argument 919 client_secret_certificate_thumbprint=None, # pylint: disable=unused-argument 920 allowed_audiences=None, issuer=None, facebook_app_id=None, # pylint: disable=unused-argument 921 facebook_app_secret=None, facebook_oauth_scopes=None, # pylint: disable=unused-argument 922 twitter_consumer_key=None, twitter_consumer_secret=None, # pylint: disable=unused-argument 923 google_client_id=None, google_client_secret=None, # pylint: disable=unused-argument 924 google_oauth_scopes=None, microsoft_account_client_id=None, # pylint: disable=unused-argument 925 microsoft_account_client_secret=None, # pylint: disable=unused-argument 926 microsoft_account_oauth_scopes=None, slot=None): # pylint: disable=unused-argument 927 auth_settings = get_auth_settings(cmd, resource_group_name, name, slot) 928 UnauthenticatedClientAction = cmd.get_models('UnauthenticatedClientAction') 929 if action == 'AllowAnonymous': 930 auth_settings.unauthenticated_client_action = UnauthenticatedClientAction.allow_anonymous 931 elif action: 932 auth_settings.unauthenticated_client_action = UnauthenticatedClientAction.redirect_to_login_page 933 auth_settings.default_provider = AUTH_TYPES[action] 934 # validate runtime version 935 if not is_auth_runtime_version_valid(runtime_version): 936 raise CLIError('Usage Error: --runtime-version set to invalid value') 937 938 import inspect 939 frame = inspect.currentframe() 940 bool_flags = ['enabled', 'token_store_enabled'] 941 # note: getargvalues is used already in azure.cli.core.commands. 942 # and no simple functional replacement for this deprecating method for 3.5 943 args, _, _, values = inspect.getargvalues(frame) # pylint: disable=deprecated-method 944 945 for arg in args[2:]: 946 if values.get(arg, None): 947 setattr(auth_settings, arg, values[arg] if arg not in bool_flags else values[arg] == 'true') 948 949 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'update_auth_settings', slot, auth_settings) 950 951 952def list_instances(cmd, resource_group_name, name, slot=None): 953 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'list_instance_identifiers', slot) 954 955 956# Currently using hardcoded values instead of this function. This function calls the stacks API; 957# Stacks API is updated with Antares deployments, 958# which are infrequent and don't line up with stacks EOL schedule. 959def list_runtimes(cmd, linux=False): 960 client = web_client_factory(cmd.cli_ctx) 961 runtime_helper = _StackRuntimeHelper(cmd=cmd, client=client, linux=linux) 962 963 return [s['displayName'] for s in runtime_helper.stacks] 964 965 966def list_runtimes_hardcoded(linux=False): 967 if linux: 968 return [s['displayName'] for s in get_file_json(RUNTIME_STACKS)['linux']] 969 return [s['displayName'] for s in get_file_json(RUNTIME_STACKS)['windows']] 970 971 972def delete_function_app(cmd, resource_group_name, name, slot=None): 973 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'delete', slot) 974 975 976def delete_webapp(cmd, resource_group_name, name, keep_metrics=None, keep_empty_plan=None, 977 keep_dns_registration=None, slot=None): # pylint: disable=unused-argument 978 client = web_client_factory(cmd.cli_ctx) 979 if slot: 980 client.web_apps.delete_slot(resource_group_name, name, slot, 981 delete_metrics=False if keep_metrics else None, 982 delete_empty_server_farm=False if keep_empty_plan else None) 983 else: 984 client.web_apps.delete(resource_group_name, name, 985 delete_metrics=False if keep_metrics else None, 986 delete_empty_server_farm=False if keep_empty_plan else None) 987 988 989def stop_webapp(cmd, resource_group_name, name, slot=None): 990 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'stop', slot) 991 992 993def start_webapp(cmd, resource_group_name, name, slot=None): 994 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'start', slot) 995 996 997def restart_webapp(cmd, resource_group_name, name, slot=None): 998 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'restart', slot) 999 1000 1001def get_site_configs(cmd, resource_group_name, name, slot=None): 1002 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get_configuration', slot) 1003 1004 1005def get_app_settings(cmd, resource_group_name, name, slot=None): 1006 result = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'list_application_settings', slot) 1007 client = web_client_factory(cmd.cli_ctx) 1008 slot_app_setting_names = client.web_apps.list_slot_configuration_names(resource_group_name, name).app_setting_names 1009 return _build_app_settings_output(result.properties, slot_app_setting_names) 1010 1011 1012# Check if the app setting is propagated to the Kudu site correctly by calling api/settings endpoint 1013# should_have [] is a list of app settings which are expected to be set 1014# should_not_have [] is a list of app settings which are expected to be absent 1015# should_contain {} is a dictionary of app settings which are expected to be set with precise values 1016# Return True if validation succeeded 1017def validate_app_settings_in_scm(cmd, resource_group_name, name, slot=None, 1018 should_have=None, should_not_have=None, should_contain=None): 1019 scm_settings = _get_app_settings_from_scm(cmd, resource_group_name, name, slot) 1020 scm_setting_keys = set(scm_settings.keys()) 1021 1022 if should_have and not set(should_have).issubset(scm_setting_keys): 1023 return False 1024 1025 if should_not_have and set(should_not_have).intersection(scm_setting_keys): 1026 return False 1027 1028 temp_setting = scm_settings.copy() 1029 temp_setting.update(should_contain or {}) 1030 if temp_setting != scm_settings: 1031 return False 1032 1033 return True 1034 1035 1036@retryable_method(3, 5) 1037def _get_app_settings_from_scm(cmd, resource_group_name, name, slot=None): 1038 scm_url = _get_scm_url(cmd, resource_group_name, name, slot) 1039 settings_url = '{}/api/settings'.format(scm_url) 1040 username, password = _get_site_credential(cmd.cli_ctx, resource_group_name, name, slot) 1041 headers = { 1042 'Content-Type': 'application/octet-stream', 1043 'Cache-Control': 'no-cache', 1044 'User-Agent': get_az_user_agent() 1045 } 1046 1047 import requests 1048 response = requests.get(settings_url, headers=headers, auth=(username, password), timeout=3) 1049 1050 return response.json() or {} 1051 1052 1053def get_connection_strings(cmd, resource_group_name, name, slot=None): 1054 result = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'list_connection_strings', slot) 1055 client = web_client_factory(cmd.cli_ctx) 1056 slot_constr_names = client.web_apps.list_slot_configuration_names(resource_group_name, name) \ 1057 .connection_string_names or [] 1058 result = [{'name': p, 1059 'value': result.properties[p].value, 1060 'type':result.properties[p].type, 1061 'slotSetting': p in slot_constr_names} for p in result.properties] 1062 return result 1063 1064 1065def get_azure_storage_accounts(cmd, resource_group_name, name, slot=None): 1066 client = web_client_factory(cmd.cli_ctx) 1067 result = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 1068 'list_azure_storage_accounts', slot) 1069 1070 slot_azure_storage_config_names = client.web_apps.list_slot_configuration_names(resource_group_name, name) \ 1071 .azure_storage_config_names or [] 1072 1073 return [{'name': p, 1074 'value': result.properties[p], 1075 'slotSetting': p in slot_azure_storage_config_names} for p in result.properties] 1076 1077 1078def _fill_ftp_publishing_url(cmd, webapp, resource_group_name, name, slot=None): 1079 profiles = list_publish_profiles(cmd, resource_group_name, name, slot) 1080 try: 1081 url = next(p['publishUrl'] for p in profiles if p['publishMethod'] == 'FTP') 1082 setattr(webapp, 'ftpPublishingUrl', url) 1083 except StopIteration: 1084 pass 1085 return webapp 1086 1087 1088def _format_fx_version(custom_image_name, container_config_type=None): 1089 lower_custom_image_name = custom_image_name.lower() 1090 if "https://" in lower_custom_image_name or "http://" in lower_custom_image_name: 1091 custom_image_name = lower_custom_image_name.replace("https://", "").replace("http://", "") 1092 fx_version = custom_image_name.strip() 1093 fx_version_lower = fx_version.lower() 1094 # handles case of only spaces 1095 if fx_version: 1096 if container_config_type: 1097 fx_version = '{}|{}'.format(container_config_type, custom_image_name) 1098 elif not fx_version_lower.startswith('docker|'): 1099 fx_version = '{}|{}'.format('DOCKER', custom_image_name) 1100 else: 1101 fx_version = ' ' 1102 return fx_version 1103 1104 1105def _add_fx_version(cmd, resource_group_name, name, custom_image_name, slot=None): 1106 fx_version = _format_fx_version(custom_image_name) 1107 web_app = get_webapp(cmd, resource_group_name, name, slot) 1108 if not web_app: 1109 raise CLIError("'{}' app doesn't exist in resource group {}".format(name, resource_group_name)) 1110 linux_fx = fx_version if (web_app.reserved or not web_app.is_xenon) else None 1111 windows_fx = fx_version if web_app.is_xenon else None 1112 return update_site_configs(cmd, resource_group_name, name, 1113 linux_fx_version=linux_fx, windows_fx_version=windows_fx, slot=slot) 1114 1115 1116def _delete_linux_fx_version(cmd, resource_group_name, name, slot=None): 1117 return update_site_configs(cmd, resource_group_name, name, linux_fx_version=' ', slot=slot) 1118 1119 1120def _get_fx_version(cmd, resource_group_name, name, slot=None): 1121 site_config = get_site_configs(cmd, resource_group_name, name, slot) 1122 return site_config.linux_fx_version or site_config.windows_fx_version or '' 1123 1124 1125def url_validator(url): 1126 try: 1127 result = urlparse(url) 1128 return all([result.scheme, result.netloc, result.path]) 1129 except ValueError: 1130 return False 1131 1132 1133def _get_linux_multicontainer_decoded_config(cmd, resource_group_name, name, slot=None): 1134 from base64 import b64decode 1135 linux_fx_version = _get_fx_version(cmd, resource_group_name, name, slot) 1136 if not any(linux_fx_version.startswith(s) for s in MULTI_CONTAINER_TYPES): 1137 raise CLIError("Cannot decode config that is not one of the" 1138 " following types: {}".format(','.join(MULTI_CONTAINER_TYPES))) 1139 return b64decode(linux_fx_version.split('|')[1].encode('utf-8')) 1140 1141 1142def _get_linux_multicontainer_encoded_config_from_file(file_name): 1143 from base64 import b64encode 1144 config_file_bytes = None 1145 if url_validator(file_name): 1146 response = urlopen(file_name, context=_ssl_context()) 1147 config_file_bytes = response.read() 1148 else: 1149 with open(file_name, 'rb') as f: 1150 config_file_bytes = f.read() 1151 # Decode base64 encoded byte array into string 1152 return b64encode(config_file_bytes).decode('utf-8') 1153 1154 1155# for any modifications to the non-optional parameters, adjust the reflection logic accordingly 1156# in the method 1157# pylint: disable=unused-argument 1158def update_site_configs(cmd, resource_group_name, name, slot=None, number_of_workers=None, linux_fx_version=None, 1159 windows_fx_version=None, pre_warmed_instance_count=None, php_version=None, 1160 python_version=None, net_framework_version=None, 1161 java_version=None, java_container=None, java_container_version=None, 1162 remote_debugging_enabled=None, web_sockets_enabled=None, 1163 always_on=None, auto_heal_enabled=None, 1164 use32_bit_worker_process=None, 1165 min_tls_version=None, 1166 http20_enabled=None, 1167 app_command_line=None, 1168 ftps_state=None, 1169 vnet_route_all_enabled=None, 1170 generic_configurations=None): 1171 configs = get_site_configs(cmd, resource_group_name, name, slot) 1172 if number_of_workers is not None: 1173 number_of_workers = validate_range_of_int_flag('--number-of-workers', number_of_workers, min_val=0, max_val=20) 1174 if linux_fx_version: 1175 if linux_fx_version.strip().lower().startswith('docker|'): 1176 update_app_settings(cmd, resource_group_name, name, ["WEBSITES_ENABLE_APP_SERVICE_STORAGE=false"]) 1177 else: 1178 delete_app_settings(cmd, resource_group_name, name, ["WEBSITES_ENABLE_APP_SERVICE_STORAGE"]) 1179 1180 if pre_warmed_instance_count is not None: 1181 pre_warmed_instance_count = validate_range_of_int_flag('--prewarmed-instance-count', pre_warmed_instance_count, 1182 min_val=0, max_val=20) 1183 import inspect 1184 frame = inspect.currentframe() 1185 bool_flags = ['remote_debugging_enabled', 'web_sockets_enabled', 'always_on', 1186 'auto_heal_enabled', 'use32_bit_worker_process', 'http20_enabled', 'vnet_route_all_enabled'] 1187 int_flags = ['pre_warmed_instance_count', 'number_of_workers'] 1188 # note: getargvalues is used already in azure.cli.core.commands. 1189 # and no simple functional replacement for this deprecating method for 3.5 1190 args, _, _, values = inspect.getargvalues(frame) # pylint: disable=deprecated-method 1191 for arg in args[3:]: 1192 if arg in int_flags and values[arg] is not None: 1193 values[arg] = validate_and_convert_to_int(arg, values[arg]) 1194 if arg != 'generic_configurations' and values.get(arg, None): 1195 setattr(configs, arg, values[arg] if arg not in bool_flags else values[arg] == 'true') 1196 1197 generic_configurations = generic_configurations or [] 1198 # https://github.com/Azure/azure-cli/issues/14857 1199 updating_ip_security_restrictions = False 1200 1201 result = {} 1202 for s in generic_configurations: 1203 try: 1204 json_object = get_json_object(s) 1205 for config_name in json_object: 1206 if config_name.lower() == 'ip_security_restrictions': 1207 updating_ip_security_restrictions = True 1208 result.update(json_object) 1209 except CLIError: 1210 config_name, value = s.split('=', 1) 1211 result[config_name] = value 1212 1213 for config_name, value in result.items(): 1214 if config_name.lower() == 'ip_security_restrictions': 1215 updating_ip_security_restrictions = True 1216 setattr(configs, config_name, value) 1217 1218 if not updating_ip_security_restrictions: 1219 setattr(configs, 'ip_security_restrictions', None) 1220 setattr(configs, 'scm_ip_security_restrictions', None) 1221 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'update_configuration', slot, configs) 1222 1223 1224def delete_app_settings(cmd, resource_group_name, name, setting_names, slot=None): 1225 app_settings = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'list_application_settings', slot) 1226 client = web_client_factory(cmd.cli_ctx) 1227 1228 slot_cfg_names = client.web_apps.list_slot_configuration_names(resource_group_name, name) 1229 is_slot_settings = False 1230 for setting_name in setting_names: 1231 app_settings.properties.pop(setting_name, None) 1232 if slot_cfg_names.app_setting_names and setting_name in slot_cfg_names.app_setting_names: 1233 slot_cfg_names.app_setting_names.remove(setting_name) 1234 is_slot_settings = True 1235 1236 if is_slot_settings: 1237 client.web_apps.update_slot_configuration_names(resource_group_name, name, slot_cfg_names) 1238 1239 result = _generic_settings_operation(cmd.cli_ctx, resource_group_name, name, 1240 'update_application_settings', 1241 app_settings, slot, client) 1242 1243 return _build_app_settings_output(result.properties, slot_cfg_names.app_setting_names) 1244 1245 1246def delete_azure_storage_accounts(cmd, resource_group_name, name, custom_id, slot=None): 1247 azure_storage_accounts = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 1248 'list_azure_storage_accounts', slot) 1249 client = web_client_factory(cmd.cli_ctx) 1250 1251 slot_cfg_names = client.web_apps.list_slot_configuration_names(resource_group_name, name) 1252 is_slot_settings = False 1253 1254 azure_storage_accounts.properties.pop(custom_id, None) 1255 if slot_cfg_names.azure_storage_config_names and custom_id in slot_cfg_names.azure_storage_config_names: 1256 slot_cfg_names.azure_storage_config_names.remove(custom_id) 1257 is_slot_settings = True 1258 1259 if is_slot_settings: 1260 client.web_apps.update_slot_configuration_names(resource_group_name, name, slot_cfg_names) 1261 1262 result = _generic_settings_operation(cmd.cli_ctx, resource_group_name, name, 1263 'update_azure_storage_accounts', azure_storage_accounts, 1264 slot, client) 1265 1266 return result.properties 1267 1268 1269def _ssl_context(): 1270 if sys.version_info < (3, 4) or (in_cloud_console() and sys.platform.system() == 'Windows'): 1271 try: 1272 return ssl.SSLContext(ssl.PROTOCOL_TLS) # added in python 2.7.13 and 3.6 1273 except AttributeError: 1274 return ssl.SSLContext(ssl.PROTOCOL_TLSv1) 1275 1276 return ssl.create_default_context() 1277 1278 1279def _build_app_settings_output(app_settings, slot_cfg_names): 1280 slot_cfg_names = slot_cfg_names or [] 1281 return [{'name': p, 1282 'value': app_settings[p], 1283 'slotSetting': p in slot_cfg_names} for p in _mask_creds_related_appsettings(app_settings)] 1284 1285 1286def update_connection_strings(cmd, resource_group_name, name, connection_string_type, 1287 settings=None, slot=None, slot_settings=None): 1288 from azure.mgmt.web.models import ConnStringValueTypePair 1289 if not settings and not slot_settings: 1290 raise CLIError('Usage Error: --settings |--slot-settings') 1291 1292 settings = settings or [] 1293 slot_settings = slot_settings or [] 1294 1295 conn_strings = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 1296 'list_connection_strings', slot) 1297 for name_value in settings + slot_settings: 1298 # split at the first '=', connection string should not have '=' in the name 1299 conn_string_name, value = name_value.split('=', 1) 1300 if value[0] in ["'", '"']: # strip away the quots used as separators 1301 value = value[1:-1] 1302 conn_strings.properties[conn_string_name] = ConnStringValueTypePair(value=value, 1303 type=connection_string_type) 1304 client = web_client_factory(cmd.cli_ctx) 1305 result = _generic_settings_operation(cmd.cli_ctx, resource_group_name, name, 1306 'update_connection_strings', 1307 conn_strings, slot, client) 1308 1309 if slot_settings: 1310 new_slot_setting_names = [n.split('=', 1)[0] for n in slot_settings] 1311 slot_cfg_names = client.web_apps.list_slot_configuration_names(resource_group_name, name) 1312 slot_cfg_names.connection_string_names = slot_cfg_names.connection_string_names or [] 1313 slot_cfg_names.connection_string_names += new_slot_setting_names 1314 client.web_apps.update_slot_configuration_names(resource_group_name, name, slot_cfg_names) 1315 1316 return result.properties 1317 1318 1319def delete_connection_strings(cmd, resource_group_name, name, setting_names, slot=None): 1320 conn_strings = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 1321 'list_connection_strings', slot) 1322 client = web_client_factory(cmd.cli_ctx) 1323 1324 slot_cfg_names = client.web_apps.list_slot_configuration_names(resource_group_name, name) 1325 is_slot_settings = False 1326 for setting_name in setting_names: 1327 conn_strings.properties.pop(setting_name, None) 1328 if slot_cfg_names.connection_string_names and setting_name in slot_cfg_names.connection_string_names: 1329 slot_cfg_names.connection_string_names.remove(setting_name) 1330 is_slot_settings = True 1331 1332 if is_slot_settings: 1333 client.web_apps.update_slot_configuration_names(resource_group_name, name, slot_cfg_names) 1334 1335 return _generic_settings_operation(cmd.cli_ctx, resource_group_name, name, 1336 'update_connection_strings', 1337 conn_strings, slot, client) 1338 1339 1340CONTAINER_APPSETTING_NAMES = ['DOCKER_REGISTRY_SERVER_URL', 'DOCKER_REGISTRY_SERVER_USERNAME', 1341 'DOCKER_REGISTRY_SERVER_PASSWORD', "WEBSITES_ENABLE_APP_SERVICE_STORAGE"] 1342APPSETTINGS_TO_MASK = ['DOCKER_REGISTRY_SERVER_PASSWORD'] 1343 1344 1345def update_container_settings(cmd, resource_group_name, name, docker_registry_server_url=None, 1346 docker_custom_image_name=None, docker_registry_server_user=None, 1347 websites_enable_app_service_storage=None, docker_registry_server_password=None, 1348 multicontainer_config_type=None, multicontainer_config_file=None, slot=None): 1349 settings = [] 1350 if docker_registry_server_url is not None: 1351 settings.append('DOCKER_REGISTRY_SERVER_URL=' + docker_registry_server_url) 1352 1353 if (not docker_registry_server_user and not docker_registry_server_password and 1354 docker_registry_server_url and '.azurecr.io' in docker_registry_server_url): 1355 logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') 1356 parsed = urlparse(docker_registry_server_url) 1357 registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] 1358 try: 1359 docker_registry_server_user, docker_registry_server_password = _get_acr_cred(cmd.cli_ctx, registry_name) 1360 except Exception as ex: # pylint: disable=broad-except 1361 logger.warning("Retrieving credentials failed with an exception:'%s'", ex) # consider throw if needed 1362 1363 if docker_registry_server_user is not None: 1364 settings.append('DOCKER_REGISTRY_SERVER_USERNAME=' + docker_registry_server_user) 1365 if docker_registry_server_password is not None: 1366 settings.append('DOCKER_REGISTRY_SERVER_PASSWORD=' + docker_registry_server_password) 1367 if websites_enable_app_service_storage: 1368 settings.append('WEBSITES_ENABLE_APP_SERVICE_STORAGE=' + websites_enable_app_service_storage) 1369 1370 if docker_registry_server_user or docker_registry_server_password or docker_registry_server_url or websites_enable_app_service_storage: # pylint: disable=line-too-long 1371 update_app_settings(cmd, resource_group_name, name, settings, slot) 1372 settings = get_app_settings(cmd, resource_group_name, name, slot) 1373 if docker_custom_image_name is not None: 1374 _add_fx_version(cmd, resource_group_name, name, docker_custom_image_name, slot) 1375 1376 if multicontainer_config_file and multicontainer_config_type: 1377 encoded_config_file = _get_linux_multicontainer_encoded_config_from_file(multicontainer_config_file) 1378 linux_fx_version = _format_fx_version(encoded_config_file, multicontainer_config_type) 1379 update_site_configs(cmd, resource_group_name, name, linux_fx_version=linux_fx_version, slot=slot) 1380 elif multicontainer_config_file or multicontainer_config_type: 1381 logger.warning('Must change both settings --multicontainer-config-file FILE --multicontainer-config-type TYPE') 1382 1383 return _mask_creds_related_appsettings(_filter_for_container_settings(cmd, resource_group_name, name, settings, 1384 slot=slot)) 1385 1386 1387def update_container_settings_functionapp(cmd, resource_group_name, name, docker_registry_server_url=None, 1388 docker_custom_image_name=None, docker_registry_server_user=None, 1389 docker_registry_server_password=None, slot=None): 1390 return update_container_settings(cmd, resource_group_name, name, docker_registry_server_url, 1391 docker_custom_image_name, docker_registry_server_user, None, 1392 docker_registry_server_password, multicontainer_config_type=None, 1393 multicontainer_config_file=None, slot=slot) 1394 1395 1396def _get_acr_cred(cli_ctx, registry_name): 1397 from azure.mgmt.containerregistry import ContainerRegistryManagementClient 1398 from azure.cli.core.commands.parameters import get_resources_in_subscription 1399 client = get_mgmt_service_client(cli_ctx, ContainerRegistryManagementClient).registries 1400 1401 result = get_resources_in_subscription(cli_ctx, 'Microsoft.ContainerRegistry/registries') 1402 result = [item for item in result if item.name.lower() == registry_name] 1403 if not result or len(result) > 1: 1404 raise CLIError("No resource or more than one were found with name '{}'.".format(registry_name)) 1405 resource_group_name = parse_resource_id(result[0].id)['resource_group'] 1406 1407 registry = client.get(resource_group_name, registry_name) 1408 1409 if registry.admin_user_enabled: # pylint: disable=no-member 1410 cred = client.list_credentials(resource_group_name, registry_name) 1411 return cred.username, cred.passwords[0].value 1412 raise CLIError("Failed to retrieve container registry credentials. Please either provide the " 1413 "credentials or run 'az acr update -n {} --admin-enabled true' to enable " 1414 "admin first.".format(registry_name)) 1415 1416 1417def delete_container_settings(cmd, resource_group_name, name, slot=None): 1418 _delete_linux_fx_version(cmd, resource_group_name, name, slot) 1419 delete_app_settings(cmd, resource_group_name, name, CONTAINER_APPSETTING_NAMES, slot) 1420 1421 1422def show_container_settings(cmd, resource_group_name, name, show_multicontainer_config=None, slot=None): 1423 settings = get_app_settings(cmd, resource_group_name, name, slot) 1424 return _mask_creds_related_appsettings(_filter_for_container_settings(cmd, resource_group_name, name, settings, 1425 show_multicontainer_config, slot)) 1426 1427 1428def show_container_settings_functionapp(cmd, resource_group_name, name, slot=None): 1429 return show_container_settings(cmd, resource_group_name, name, show_multicontainer_config=None, slot=slot) 1430 1431 1432def _filter_for_container_settings(cmd, resource_group_name, name, settings, 1433 show_multicontainer_config=None, slot=None): 1434 result = [x for x in settings if x['name'] in CONTAINER_APPSETTING_NAMES] 1435 fx_version = _get_fx_version(cmd, resource_group_name, name, slot).strip() 1436 if fx_version: 1437 added_image_name = {'name': 'DOCKER_CUSTOM_IMAGE_NAME', 1438 'value': fx_version} 1439 result.append(added_image_name) 1440 if show_multicontainer_config: 1441 decoded_value = _get_linux_multicontainer_decoded_config(cmd, resource_group_name, name, slot) 1442 decoded_image_name = {'name': 'DOCKER_CUSTOM_IMAGE_NAME_DECODED', 1443 'value': decoded_value} 1444 result.append(decoded_image_name) 1445 return result 1446 1447 1448# TODO: remove this when #3660(service tracking issue) is resolved 1449def _mask_creds_related_appsettings(settings): 1450 for x in [x1 for x1 in settings if x1 in APPSETTINGS_TO_MASK]: 1451 settings[x] = None 1452 return settings 1453 1454 1455def add_hostname(cmd, resource_group_name, webapp_name, hostname, slot=None): 1456 from azure.mgmt.web.models import HostNameBinding 1457 client = web_client_factory(cmd.cli_ctx) 1458 webapp = client.web_apps.get(resource_group_name, webapp_name) 1459 if not webapp: 1460 raise CLIError("'{}' app doesn't exist".format(webapp_name)) 1461 binding = HostNameBinding(site_name=webapp.name) 1462 if slot is None: 1463 return client.web_apps.create_or_update_host_name_binding(resource_group_name=resource_group_name, 1464 name=webapp.name, host_name=hostname, 1465 host_name_binding=binding) 1466 1467 return client.web_apps.create_or_update_host_name_binding_slot(resource_group_name=resource_group_name, 1468 name=webapp.name, host_name=hostname, 1469 slot=slot, host_name_binding=binding) 1470 1471 1472def delete_hostname(cmd, resource_group_name, webapp_name, hostname, slot=None): 1473 client = web_client_factory(cmd.cli_ctx) 1474 if slot is None: 1475 return client.web_apps.delete_host_name_binding(resource_group_name, webapp_name, hostname) 1476 1477 return client.web_apps.delete_host_name_binding_slot(resource_group_name, webapp_name, slot, hostname) 1478 1479 1480def list_hostnames(cmd, resource_group_name, webapp_name, slot=None): 1481 result = list(_generic_site_operation(cmd.cli_ctx, resource_group_name, webapp_name, 1482 'list_host_name_bindings', slot)) 1483 for r in result: 1484 r.name = r.name.split('/')[-1] 1485 return result 1486 1487 1488def get_external_ip(cmd, resource_group_name, webapp_name): 1489 SslState = cmd.get_models('SslState') 1490 # logics here are ported from portal 1491 client = web_client_factory(cmd.cli_ctx) 1492 webapp = client.web_apps.get(resource_group_name, webapp_name) 1493 if not webapp: 1494 raise CLIError("'{}' app doesn't exist".format(webapp_name)) 1495 if webapp.hosting_environment_profile: 1496 address = client.app_service_environments.list_vips( 1497 resource_group_name, webapp.hosting_environment_profile.name) 1498 if address.internal_ip_address: 1499 ip_address = address.internal_ip_address 1500 else: 1501 vip = next((s for s in webapp.host_name_ssl_states if s.ssl_state == SslState.ip_based_enabled), None) 1502 ip_address = vip.virtual_ip if vip else address.service_ip_address 1503 else: 1504 ip_address = _resolve_hostname_through_dns(webapp.default_host_name) 1505 1506 return {'ip': ip_address} 1507 1508 1509def _resolve_hostname_through_dns(hostname): 1510 import socket 1511 return socket.gethostbyname(hostname) 1512 1513 1514def create_webapp_slot(cmd, resource_group_name, webapp, slot, configuration_source=None): 1515 Site, SiteConfig, NameValuePair = cmd.get_models('Site', 'SiteConfig', 'NameValuePair') 1516 client = web_client_factory(cmd.cli_ctx) 1517 site = client.web_apps.get(resource_group_name, webapp) 1518 site_config = get_site_configs(cmd, resource_group_name, webapp, None) 1519 if not site: 1520 raise CLIError("'{}' app doesn't exist".format(webapp)) 1521 if 'functionapp' in site.kind: 1522 raise CLIError("'{}' is a function app. Please use `az functionapp deployment slot create`.".format(webapp)) 1523 location = site.location 1524 slot_def = Site(server_farm_id=site.server_farm_id, location=location) 1525 slot_def.site_config = SiteConfig() 1526 1527 # if it is a Windows Container site, at least pass the necessary 1528 # app settings to perform the container image validation: 1529 if configuration_source and site_config.windows_fx_version: 1530 # get settings from the source 1531 clone_from_prod = configuration_source.lower() == webapp.lower() 1532 src_slot = None if clone_from_prod else configuration_source 1533 app_settings = _generic_site_operation(cmd.cli_ctx, resource_group_name, webapp, 1534 'list_application_settings', src_slot) 1535 settings = [] 1536 for k, v in app_settings.properties.items(): 1537 if k in ("DOCKER_REGISTRY_SERVER_USERNAME", "DOCKER_REGISTRY_SERVER_PASSWORD", 1538 "DOCKER_REGISTRY_SERVER_URL"): 1539 settings.append(NameValuePair(name=k, value=v)) 1540 slot_def.site_config = SiteConfig(app_settings=settings) 1541 poller = client.web_apps.begin_create_or_update_slot(resource_group_name, webapp, site_envelope=slot_def, slot=slot) 1542 result = LongRunningOperation(cmd.cli_ctx)(poller) 1543 1544 if configuration_source: 1545 update_slot_configuration_from_source(cmd, client, resource_group_name, webapp, slot, configuration_source) 1546 1547 result.name = result.name.split('/')[-1] 1548 return result 1549 1550 1551def create_functionapp_slot(cmd, resource_group_name, name, slot, configuration_source=None): 1552 Site = cmd.get_models('Site') 1553 client = web_client_factory(cmd.cli_ctx) 1554 site = client.web_apps.get(resource_group_name, name) 1555 if not site: 1556 raise CLIError("'{}' function app doesn't exist".format(name)) 1557 location = site.location 1558 slot_def = Site(server_farm_id=site.server_farm_id, location=location) 1559 1560 poller = client.web_apps.begin_create_or_update_slot(resource_group_name, name, site_envelope=slot_def, slot=slot) 1561 result = LongRunningOperation(cmd.cli_ctx)(poller) 1562 1563 if configuration_source: 1564 update_slot_configuration_from_source(cmd, client, resource_group_name, name, slot, configuration_source) 1565 1566 result.name = result.name.split('/')[-1] 1567 return result 1568 1569 1570def update_slot_configuration_from_source(cmd, client, resource_group_name, webapp, slot, configuration_source=None): 1571 clone_from_prod = configuration_source.lower() == webapp.lower() 1572 site_config = get_site_configs(cmd, resource_group_name, webapp, 1573 None if clone_from_prod else configuration_source) 1574 _generic_site_operation(cmd.cli_ctx, resource_group_name, webapp, 1575 'update_configuration', slot, site_config) 1576 1577 # slot create doesn't clone over the app-settings and connection-strings, so we do it here 1578 # also make sure slot settings don't get propagated. 1579 1580 slot_cfg_names = client.web_apps.list_slot_configuration_names(resource_group_name, webapp) 1581 src_slot = None if clone_from_prod else configuration_source 1582 app_settings = _generic_site_operation(cmd.cli_ctx, resource_group_name, webapp, 1583 'list_application_settings', 1584 src_slot) 1585 for a in slot_cfg_names.app_setting_names or []: 1586 app_settings.properties.pop(a, None) 1587 1588 connection_strings = _generic_site_operation(cmd.cli_ctx, resource_group_name, webapp, 1589 'list_connection_strings', 1590 src_slot) 1591 for a in slot_cfg_names.connection_string_names or []: 1592 connection_strings.properties.pop(a, None) 1593 1594 _generic_settings_operation(cmd.cli_ctx, resource_group_name, webapp, 1595 'update_application_settings', 1596 app_settings, slot, client) 1597 _generic_settings_operation(cmd.cli_ctx, resource_group_name, webapp, 1598 'update_connection_strings', 1599 connection_strings, slot, client) 1600 1601 1602def config_source_control(cmd, resource_group_name, name, repo_url, repository_type='git', branch=None, # pylint: disable=too-many-locals 1603 manual_integration=None, git_token=None, slot=None, github_action=None): 1604 client = web_client_factory(cmd.cli_ctx) 1605 location = _get_location_from_webapp(client, resource_group_name, name) 1606 1607 from azure.mgmt.web.models import SiteSourceControl, SourceControl 1608 if git_token: 1609 sc = SourceControl(location=location, source_control_name='GitHub', token=git_token) 1610 client.update_source_control('GitHub', sc) 1611 1612 source_control = SiteSourceControl(location=location, repo_url=repo_url, branch=branch, 1613 is_manual_integration=manual_integration, 1614 is_mercurial=(repository_type != 'git'), is_git_hub_action=bool(github_action)) 1615 1616 # SCC config can fail if previous commands caused SCMSite shutdown, so retry here. 1617 for i in range(5): 1618 try: 1619 poller = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 1620 'begin_create_or_update_source_control', 1621 slot, source_control) 1622 return LongRunningOperation(cmd.cli_ctx)(poller) 1623 except Exception as ex: # pylint: disable=broad-except 1624 import re 1625 ex = ex_handler_factory(no_throw=True)(ex) 1626 # for non server errors(50x), just throw; otherwise retry 4 times 1627 if i == 4 or not re.findall(r'\(50\d\)', str(ex)): 1628 raise 1629 logger.warning('retrying %s/4', i + 1) 1630 time.sleep(5) # retry in a moment 1631 1632 1633def update_git_token(cmd, git_token=None): 1634 ''' 1635 Update source control token cached in Azure app service. If no token is provided, 1636 the command will clean up existing token. 1637 ''' 1638 client = web_client_factory(cmd.cli_ctx) 1639 from azure.mgmt.web.models import SourceControl 1640 sc = SourceControl(name='not-really-needed', source_control_name='GitHub', token=git_token or '') 1641 return client.update_source_control('GitHub', sc) 1642 1643 1644def show_source_control(cmd, resource_group_name, name, slot=None): 1645 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get_source_control', slot) 1646 1647 1648def delete_source_control(cmd, resource_group_name, name, slot=None): 1649 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'delete_source_control', slot) 1650 1651 1652def enable_local_git(cmd, resource_group_name, name, slot=None): 1653 client = web_client_factory(cmd.cli_ctx) 1654 site_config = get_site_configs(cmd, resource_group_name, name, slot) 1655 site_config.scm_type = 'LocalGit' 1656 _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'create_or_update_configuration', slot, site_config) 1657 return {'url': _get_local_git_url(cmd.cli_ctx, client, resource_group_name, name, slot)} 1658 1659 1660def sync_site_repo(cmd, resource_group_name, name, slot=None): 1661 try: 1662 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'sync_repository', slot) 1663 except CloudError as ex: # Because of bad spec, sdk throws on 200. We capture it here 1664 if ex.status_code not in [200, 204]: 1665 raise ex 1666 1667 1668def list_app_service_plans(cmd, resource_group_name=None): 1669 client = web_client_factory(cmd.cli_ctx) 1670 if resource_group_name is None: 1671 plans = list(client.app_service_plans.list(detailed=True)) # enables querying "numberOfSites" 1672 else: 1673 plans = list(client.app_service_plans.list_by_resource_group(resource_group_name)) 1674 for plan in plans: 1675 # prune a few useless fields 1676 del plan.geo_region 1677 del plan.subscription 1678 return plans 1679 1680 1681def create_app_service_plan(cmd, resource_group_name, name, is_linux, hyper_v, per_site_scaling=False, 1682 app_service_environment=None, sku='B1', number_of_workers=None, location=None, 1683 tags=None, no_wait=False): 1684 HostingEnvironmentProfile, SkuDescription, AppServicePlan = cmd.get_models( 1685 'HostingEnvironmentProfile', 'SkuDescription', 'AppServicePlan') 1686 sku = _normalize_sku(sku) 1687 _validate_asp_sku(app_service_environment, sku) 1688 if is_linux and hyper_v: 1689 raise MutuallyExclusiveArgumentError('Usage error: --is-linux and --hyper-v cannot be used together.') 1690 1691 client = web_client_factory(cmd.cli_ctx) 1692 if app_service_environment: 1693 if hyper_v: 1694 raise ArgumentUsageError('Windows containers is not yet supported in app service environment') 1695 ase_list = client.app_service_environments.list() 1696 ase_found = False 1697 ase = None 1698 for ase in ase_list: 1699 if ase.name.lower() == app_service_environment.lower() or ase.id.lower() == app_service_environment.lower(): 1700 ase_def = HostingEnvironmentProfile(id=ase.id) 1701 location = ase.location 1702 ase_found = True 1703 break 1704 if not ase_found: 1705 err_msg = "App service environment '{}' not found in subscription.".format(app_service_environment) 1706 raise ResourceNotFoundError(err_msg) 1707 else: # Non-ASE 1708 ase_def = None 1709 if location is None: 1710 location = _get_location_from_resource_group(cmd.cli_ctx, resource_group_name) 1711 1712 # the api is odd on parameter naming, have to live with it for now 1713 sku_def = SkuDescription(tier=get_sku_name(sku), name=sku, capacity=number_of_workers) 1714 plan_def = AppServicePlan(location=location, tags=tags, sku=sku_def, 1715 reserved=(is_linux or None), hyper_v=(hyper_v or None), name=name, 1716 per_site_scaling=per_site_scaling, hosting_environment_profile=ase_def) 1717 return sdk_no_wait(no_wait, client.app_service_plans.begin_create_or_update, name=name, 1718 resource_group_name=resource_group_name, app_service_plan=plan_def) 1719 1720 1721def update_app_service_plan(instance, sku=None, number_of_workers=None): 1722 if number_of_workers is None and sku is None: 1723 logger.warning('No update is done. Specify --sku and/or --number-of-workers.') 1724 sku_def = instance.sku 1725 if sku is not None: 1726 sku = _normalize_sku(sku) 1727 sku_def.tier = get_sku_name(sku) 1728 sku_def.name = sku 1729 1730 if number_of_workers is not None: 1731 sku_def.capacity = number_of_workers 1732 instance.sku = sku_def 1733 return instance 1734 1735 1736def show_plan(cmd, resource_group_name, name): 1737 from azure.cli.core.commands.client_factory import get_subscription_id 1738 client = web_client_factory(cmd.cli_ctx) 1739 serverfarm_url_base = 'subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/serverfarms/{}?api-version={}' 1740 subscription_id = get_subscription_id(cmd.cli_ctx) 1741 serverfarm_url = serverfarm_url_base.format(subscription_id, resource_group_name, name, client.DEFAULT_API_VERSION) 1742 request_url = cmd.cli_ctx.cloud.endpoints.resource_manager + serverfarm_url 1743 response = send_raw_request(cmd.cli_ctx, "GET", request_url) 1744 return response.json() 1745 1746 1747def update_functionapp_app_service_plan(cmd, instance, sku=None, number_of_workers=None, max_burst=None): 1748 instance = update_app_service_plan(instance, sku, number_of_workers) 1749 if max_burst is not None: 1750 if not is_plan_elastic_premium(cmd, instance): 1751 raise CLIError("Usage error: --max-burst is only supported for Elastic Premium (EP) plans") 1752 max_burst = validate_range_of_int_flag('--max-burst', max_burst, min_val=0, max_val=20) 1753 instance.maximum_elastic_worker_count = max_burst 1754 if number_of_workers is not None: 1755 number_of_workers = validate_range_of_int_flag('--number-of-workers / --min-instances', 1756 number_of_workers, min_val=0, max_val=20) 1757 return update_app_service_plan(instance, sku, number_of_workers) 1758 1759 1760def show_backup_configuration(cmd, resource_group_name, webapp_name, slot=None): 1761 try: 1762 return _generic_site_operation(cmd.cli_ctx, resource_group_name, webapp_name, 1763 'get_backup_configuration', slot) 1764 except Exception: # pylint: disable=broad-except 1765 raise CLIError('Backup configuration not found') 1766 1767 1768def list_backups(cmd, resource_group_name, webapp_name, slot=None): 1769 return _generic_site_operation(cmd.cli_ctx, resource_group_name, webapp_name, 'list_backups', slot) 1770 1771 1772def create_backup(cmd, resource_group_name, webapp_name, storage_account_url, 1773 db_name=None, db_type=None, 1774 db_connection_string=None, backup_name=None, slot=None): 1775 BackupRequest = cmd.get_models('BackupRequest') 1776 client = web_client_factory(cmd.cli_ctx) 1777 if backup_name and backup_name.lower().endswith('.zip'): 1778 backup_name = backup_name[:-4] 1779 db_setting = _create_db_setting(cmd, db_name, db_type=db_type, db_connection_string=db_connection_string) 1780 backup_request = BackupRequest(backup_name=backup_name, 1781 storage_account_url=storage_account_url, databases=db_setting) 1782 if slot: 1783 return client.web_apps.backup_slot(resource_group_name, webapp_name, backup_request, slot) 1784 1785 return client.web_apps.backup(resource_group_name, webapp_name, backup_request) 1786 1787 1788def update_backup_schedule(cmd, resource_group_name, webapp_name, storage_account_url=None, 1789 frequency=None, keep_at_least_one_backup=None, 1790 retention_period_in_days=None, db_name=None, 1791 db_connection_string=None, db_type=None, backup_name=None, slot=None): 1792 BackupSchedule, BackupRequest = cmd.get_models('BackupSchedule', 'BackupRequest') 1793 configuration = None 1794 if backup_name and backup_name.lower().endswith('.zip'): 1795 backup_name = backup_name[:-4] 1796 if not backup_name: 1797 backup_name = '{0}_{1}'.format(webapp_name, datetime.datetime.utcnow().strftime('%Y%m%d%H%M')) 1798 1799 try: 1800 configuration = _generic_site_operation(cmd.cli_ctx, resource_group_name, webapp_name, 1801 'get_backup_configuration', slot) 1802 except Exception: # pylint: disable=broad-except 1803 # No configuration set yet 1804 if not all([storage_account_url, frequency, retention_period_in_days, 1805 keep_at_least_one_backup]): 1806 raise CLIError('No backup configuration found. A configuration must be created. ' + 1807 'Usage: --container-url URL --frequency TIME --retention DAYS ' + 1808 '--retain-one TRUE/FALSE') 1809 1810 # If arguments were not specified, use the values in the current backup schedule 1811 if storage_account_url is None: 1812 storage_account_url = configuration.storage_account_url 1813 1814 if retention_period_in_days is None: 1815 retention_period_in_days = configuration.backup_schedule.retention_period_in_days 1816 1817 if keep_at_least_one_backup is None: 1818 keep_at_least_one_backup = configuration.backup_schedule.keep_at_least_one_backup 1819 else: 1820 keep_at_least_one_backup = keep_at_least_one_backup.lower() == 'true' 1821 1822 if frequency: 1823 # Parse schedule frequency 1824 frequency_num, frequency_unit = _parse_frequency(cmd, frequency) 1825 else: 1826 frequency_num = configuration.backup_schedule.frequency_interval 1827 frequency_unit = configuration.backup_schedule.frequency_unit 1828 1829 if configuration and configuration.databases: 1830 db = configuration.databases[0] 1831 db_type = db_type or db.database_type 1832 db_name = db_name or db.name 1833 db_connection_string = db_connection_string or db.connection_string 1834 1835 db_setting = _create_db_setting(cmd, db_name, db_type=db_type, db_connection_string=db_connection_string) 1836 1837 backup_schedule = BackupSchedule(frequency_interval=frequency_num, frequency_unit=frequency_unit.name, 1838 keep_at_least_one_backup=keep_at_least_one_backup, 1839 retention_period_in_days=retention_period_in_days) 1840 backup_request = BackupRequest(backup_request_name=backup_name, backup_schedule=backup_schedule, 1841 enabled=True, storage_account_url=storage_account_url, 1842 databases=db_setting) 1843 return _generic_site_operation(cmd.cli_ctx, resource_group_name, webapp_name, 'update_backup_configuration', 1844 slot, backup_request) 1845 1846 1847def restore_backup(cmd, resource_group_name, webapp_name, storage_account_url, backup_name, 1848 db_name=None, db_type=None, db_connection_string=None, 1849 target_name=None, overwrite=None, ignore_hostname_conflict=None, slot=None): 1850 RestoreRequest = cmd.get_models('RestoreRequest') 1851 client = web_client_factory(cmd.cli_ctx) 1852 storage_blob_name = backup_name 1853 if not storage_blob_name.lower().endswith('.zip'): 1854 storage_blob_name += '.zip' 1855 db_setting = _create_db_setting(cmd, db_name, db_type=db_type, db_connection_string=db_connection_string) 1856 restore_request = RestoreRequest(storage_account_url=storage_account_url, 1857 blob_name=storage_blob_name, overwrite=overwrite, 1858 site_name=target_name, databases=db_setting, 1859 ignore_conflicting_host_names=ignore_hostname_conflict) 1860 if slot: 1861 return client.web_apps.restore_slot(resource_group_name, webapp_name, 0, restore_request, slot) 1862 1863 return client.web_apps.restore(resource_group_name, webapp_name, 0, restore_request) 1864 1865 1866def list_snapshots(cmd, resource_group_name, name, slot=None): 1867 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'list_snapshots', 1868 slot) 1869 1870 1871def restore_snapshot(cmd, resource_group_name, name, time, slot=None, restore_content_only=False, # pylint: disable=redefined-outer-name 1872 source_resource_group=None, source_name=None, source_slot=None): 1873 from azure.cli.core.commands.client_factory import get_subscription_id 1874 SnapshotRecoverySource, SnapshotRestoreRequest = cmd.get_models('SnapshotRecoverySource', 'SnapshotRestoreRequest') 1875 client = web_client_factory(cmd.cli_ctx) 1876 recover_config = not restore_content_only 1877 if all([source_resource_group, source_name]): 1878 # Restore from source app to target app 1879 sub_id = get_subscription_id(cmd.cli_ctx) 1880 source_id = "/subscriptions/" + sub_id + "/resourceGroups/" + source_resource_group + \ 1881 "/providers/Microsoft.Web/sites/" + source_name 1882 if source_slot: 1883 source_id = source_id + "/slots/" + source_slot 1884 source = SnapshotRecoverySource(id=source_id) 1885 request = SnapshotRestoreRequest(overwrite=False, snapshot_time=time, recovery_source=source, 1886 recover_configuration=recover_config) 1887 if slot: 1888 return client.web_apps.restore_snapshot_slot(resource_group_name, name, request, slot) 1889 return client.web_apps.restore_snapshot(resource_group_name, name, request) 1890 if any([source_resource_group, source_name]): 1891 raise CLIError('usage error: --source-resource-group and --source-name must both be specified if one is used') 1892 # Overwrite app with its own snapshot 1893 request = SnapshotRestoreRequest(overwrite=True, snapshot_time=time, recover_configuration=recover_config) 1894 if slot: 1895 return client.web_apps.restore_snapshot_slot(resource_group_name, name, request, slot) 1896 return client.web_apps.restore_snapshot(resource_group_name, name, request) 1897 1898 1899# pylint: disable=inconsistent-return-statements 1900def _create_db_setting(cmd, db_name, db_type, db_connection_string): 1901 DatabaseBackupSetting = cmd.get_models('DatabaseBackupSetting') 1902 if all([db_name, db_type, db_connection_string]): 1903 return [DatabaseBackupSetting(database_type=db_type, name=db_name, connection_string=db_connection_string)] 1904 if any([db_name, db_type, db_connection_string]): 1905 raise CLIError('usage error: --db-name NAME --db-type TYPE --db-connection-string STRING') 1906 1907 1908def _parse_frequency(cmd, frequency): 1909 FrequencyUnit = cmd.get_models('FrequencyUnit') 1910 unit_part = frequency.lower()[-1] 1911 if unit_part == 'd': 1912 frequency_unit = FrequencyUnit.day 1913 elif unit_part == 'h': 1914 frequency_unit = FrequencyUnit.hour 1915 else: 1916 raise CLIError('Frequency must end with d or h for "day" or "hour"') 1917 1918 try: 1919 frequency_num = int(frequency[:-1]) 1920 except ValueError: 1921 raise CLIError('Frequency must start with a number') 1922 1923 if frequency_num < 0: 1924 raise CLIError('Frequency must be positive') 1925 1926 return frequency_num, frequency_unit 1927 1928 1929def _get_deleted_apps_locations(cli_ctx): 1930 client = get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES) 1931 web_provider = client.providers.get('Microsoft.Web') 1932 del_sites_resource = next((x for x in web_provider.resource_types if x.resource_type == 'deletedSites'), None) 1933 if del_sites_resource: 1934 return del_sites_resource.locations 1935 return [] 1936 1937 1938def _get_local_git_url(cli_ctx, client, resource_group_name, name, slot=None): 1939 user = client.get_publishing_user() 1940 result = _generic_site_operation(cli_ctx, resource_group_name, name, 'get_source_control', slot) 1941 parsed = urlparse(result.repo_url) 1942 return '{}://{}@{}/{}.git'.format(parsed.scheme, user.publishing_user_name, 1943 parsed.netloc, name) 1944 1945 1946def _get_scm_url(cmd, resource_group_name, name, slot=None): 1947 from azure.mgmt.web.models import HostType 1948 app = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot) 1949 for host in app.host_name_ssl_states or []: 1950 if host.host_type == HostType.repository: 1951 return "https://{}".format(host.name) 1952 1953 # this should not happen, but throw anyway 1954 raise ValueError('Failed to retrieve Scm Uri') 1955 1956 1957def get_publishing_user(cmd): 1958 client = web_client_factory(cmd.cli_ctx) 1959 return client.get_publishing_user() 1960 1961 1962def set_deployment_user(cmd, user_name, password=None): 1963 ''' 1964 Update deployment credentials.(Note, all webapps in your subscription will be impacted) 1965 ''' 1966 User = cmd.get_models('User') 1967 client = web_client_factory(cmd.cli_ctx) 1968 user = User(publishing_user_name=user_name) 1969 if password is None: 1970 try: 1971 password = prompt_pass(msg='Password: ', confirm=True) 1972 except NoTTYException: 1973 raise CLIError('Please specify both username and password in non-interactive mode.') 1974 1975 user.publishing_password = password 1976 return client.update_publishing_user(user) 1977 1978 1979def list_publishing_credentials(cmd, resource_group_name, name, slot=None): 1980 content = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 1981 'begin_list_publishing_credentials', slot) 1982 return content.result() 1983 1984 1985def list_publish_profiles(cmd, resource_group_name, name, slot=None, xml=False): 1986 import xmltodict 1987 content = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 1988 'list_publishing_profile_xml_with_secrets', slot, {"format": "WebDeploy"}) 1989 full_xml = '' 1990 for f in content: 1991 full_xml += f.decode() 1992 1993 if not xml: 1994 profiles = xmltodict.parse(full_xml, xml_attribs=True)['publishData']['publishProfile'] 1995 converted = [] 1996 1997 if not isinstance(profiles, list): 1998 profiles = [profiles] 1999 2000 for profile in profiles: 2001 new = {} 2002 for key in profile: 2003 # strip the leading '@' xmltodict put in for attributes 2004 new[key.lstrip('@')] = profile[key] 2005 converted.append(new) 2006 return converted 2007 2008 cmd.cli_ctx.invocation.data['output'] = 'tsv' 2009 return full_xml 2010 2011 2012def enable_cd(cmd, resource_group_name, name, enable, slot=None): 2013 settings = [] 2014 settings.append("DOCKER_ENABLE_CI=" + enable) 2015 2016 update_app_settings(cmd, resource_group_name, name, settings, slot) 2017 2018 return show_container_cd_url(cmd, resource_group_name, name, slot) 2019 2020 2021def show_container_cd_url(cmd, resource_group_name, name, slot=None): 2022 settings = get_app_settings(cmd, resource_group_name, name, slot) 2023 docker_enabled = False 2024 for setting in settings: 2025 if setting['name'] == 'DOCKER_ENABLE_CI' and setting['value'] == 'true': 2026 docker_enabled = True 2027 break 2028 2029 cd_settings = {} 2030 cd_settings['DOCKER_ENABLE_CI'] = docker_enabled 2031 2032 if docker_enabled: 2033 credentials = list_publishing_credentials(cmd, resource_group_name, name, slot) 2034 if credentials: 2035 cd_url = credentials.scm_uri + '/docker/hook' 2036 cd_settings['CI_CD_URL'] = cd_url 2037 else: 2038 cd_settings['CI_CD_URL'] = '' 2039 2040 return cd_settings 2041 2042 2043def view_in_browser(cmd, resource_group_name, name, slot=None, logs=False): 2044 url = _get_url(cmd, resource_group_name, name, slot) 2045 open_page_in_browser(url) 2046 if logs: 2047 get_streaming_log(cmd, resource_group_name, name, provider=None, slot=slot) 2048 2049 2050def _get_url(cmd, resource_group_name, name, slot=None): 2051 SslState = cmd.get_models('SslState') 2052 site = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot) 2053 if not site: 2054 raise CLIError("'{}' app doesn't exist".format(name)) 2055 url = site.enabled_host_names[0] # picks the custom domain URL incase a domain is assigned 2056 ssl_host = next((h for h in site.host_name_ssl_states 2057 if h.ssl_state != SslState.disabled), None) 2058 return ('https' if ssl_host else 'http') + '://' + url 2059 2060 2061# TODO: expose new blob suport 2062def config_diagnostics(cmd, resource_group_name, name, level=None, 2063 application_logging=None, web_server_logging=None, 2064 docker_container_logging=None, detailed_error_messages=None, 2065 failed_request_tracing=None, slot=None): 2066 from azure.mgmt.web.models import (FileSystemApplicationLogsConfig, ApplicationLogsConfig, 2067 AzureBlobStorageApplicationLogsConfig, SiteLogsConfig, 2068 HttpLogsConfig, FileSystemHttpLogsConfig, 2069 EnabledConfig) 2070 client = web_client_factory(cmd.cli_ctx) 2071 # TODO: ensure we call get_site only once 2072 site = client.web_apps.get(resource_group_name, name) 2073 if not site: 2074 raise CLIError("'{}' app doesn't exist".format(name)) 2075 location = site.location 2076 2077 application_logs = None 2078 if application_logging: 2079 fs_log = None 2080 blob_log = None 2081 level = level if application_logging != 'off' else False 2082 level = True if level is None else level 2083 if application_logging in ['filesystem', 'off']: 2084 fs_log = FileSystemApplicationLogsConfig(level=level) 2085 if application_logging in ['azureblobstorage', 'off']: 2086 blob_log = AzureBlobStorageApplicationLogsConfig(level=level, retention_in_days=3, 2087 sas_url=None) 2088 application_logs = ApplicationLogsConfig(file_system=fs_log, 2089 azure_blob_storage=blob_log) 2090 2091 http_logs = None 2092 server_logging_option = web_server_logging or docker_container_logging 2093 if server_logging_option: 2094 # TODO: az blob storage log config currently not in use, will be impelemented later. 2095 # Tracked as Issue: #4764 on Github 2096 filesystem_log_config = None 2097 turned_on = server_logging_option != 'off' 2098 if server_logging_option in ['filesystem', 'off']: 2099 # 100 mb max log size, retention lasts 3 days. Yes we hard code it, portal does too 2100 filesystem_log_config = FileSystemHttpLogsConfig(retention_in_mb=100, retention_in_days=3, 2101 enabled=turned_on) 2102 http_logs = HttpLogsConfig(file_system=filesystem_log_config, azure_blob_storage=None) 2103 2104 detailed_error_messages_logs = (None if detailed_error_messages is None 2105 else EnabledConfig(enabled=detailed_error_messages)) 2106 failed_request_tracing_logs = (None if failed_request_tracing is None 2107 else EnabledConfig(enabled=failed_request_tracing)) 2108 site_log_config = SiteLogsConfig(location=location, 2109 application_logs=application_logs, 2110 http_logs=http_logs, 2111 failed_requests_tracing=failed_request_tracing_logs, 2112 detailed_error_messages=detailed_error_messages_logs) 2113 2114 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'update_diagnostic_logs_config', 2115 slot, site_log_config) 2116 2117 2118def show_diagnostic_settings(cmd, resource_group_name, name, slot=None): 2119 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get_diagnostic_logs_configuration', slot) 2120 2121 2122def show_deployment_log(cmd, resource_group, name, slot=None, deployment_id=None): 2123 import urllib3 2124 import requests 2125 2126 scm_url = _get_scm_url(cmd, resource_group, name, slot) 2127 username, password = _get_site_credential(cmd.cli_ctx, resource_group, name, slot) 2128 headers = urllib3.util.make_headers(basic_auth='{}:{}'.format(username, password)) 2129 2130 deployment_log_url = '' 2131 if deployment_id: 2132 deployment_log_url = '{}/api/deployments/{}/log'.format(scm_url, deployment_id) 2133 else: 2134 deployments_url = '{}/api/deployments/'.format(scm_url) 2135 response = requests.get(deployments_url, headers=headers) 2136 2137 if response.status_code != 200: 2138 raise CLIError("Failed to connect to '{}' with status code '{}' and reason '{}'".format( 2139 deployments_url, response.status_code, response.reason)) 2140 2141 sorted_logs = sorted( 2142 response.json(), 2143 key=lambda x: x['start_time'], 2144 reverse=True 2145 ) 2146 if sorted_logs and sorted_logs[0]: 2147 deployment_log_url = sorted_logs[0].get('log_url', '') 2148 2149 if deployment_log_url: 2150 response = requests.get(deployment_log_url, headers=headers) 2151 if response.status_code != 200: 2152 raise CLIError("Failed to connect to '{}' with status code '{}' and reason '{}'".format( 2153 deployment_log_url, response.status_code, response.reason)) 2154 return response.json() 2155 return [] 2156 2157 2158def list_deployment_logs(cmd, resource_group, name, slot=None): 2159 scm_url = _get_scm_url(cmd, resource_group, name, slot) 2160 deployment_log_url = '{}/api/deployments/'.format(scm_url) 2161 username, password = _get_site_credential(cmd.cli_ctx, resource_group, name, slot) 2162 2163 import urllib3 2164 headers = urllib3.util.make_headers(basic_auth='{}:{}'.format(username, password)) 2165 2166 import requests 2167 response = requests.get(deployment_log_url, headers=headers) 2168 2169 if response.status_code != 200: 2170 raise CLIError("Failed to connect to '{}' with status code '{}' and reason '{}'".format( 2171 scm_url, response.status_code, response.reason)) 2172 2173 return response.json() or [] 2174 2175 2176def config_slot_auto_swap(cmd, resource_group_name, webapp, slot, auto_swap_slot=None, disable=None): 2177 client = web_client_factory(cmd.cli_ctx) 2178 site_config = client.web_apps.get_configuration_slot(resource_group_name, webapp, slot) 2179 site_config.auto_swap_slot_name = '' if disable else (auto_swap_slot or 'production') 2180 return _generic_site_operation(cmd.cli_ctx, resource_group_name, webapp, 'update_configuration', slot, site_config) 2181 2182 2183def list_slots(cmd, resource_group_name, webapp): 2184 client = web_client_factory(cmd.cli_ctx) 2185 slots = list(client.web_apps.list_slots(resource_group_name, webapp)) 2186 for slot in slots: 2187 slot.name = slot.name.split('/')[-1] 2188 setattr(slot, 'app_service_plan', parse_resource_id(slot.server_farm_id)['name']) 2189 del slot.server_farm_id 2190 return slots 2191 2192 2193def swap_slot(cmd, resource_group_name, webapp, slot, target_slot=None, preserve_vnet=None, action='swap'): 2194 client = web_client_factory(cmd.cli_ctx) 2195 # Default isPreserveVnet to 'True' if preserve_vnet is 'None' 2196 isPreserveVnet = preserve_vnet if preserve_vnet is not None else 'true' 2197 # converstion from string to Boolean 2198 isPreserveVnet = bool(isPreserveVnet == 'true') 2199 CsmSlotEntity = cmd.get_models('CsmSlotEntity') 2200 slot_swap_entity = CsmSlotEntity(target_slot=target_slot or 'production', preserve_vnet=isPreserveVnet) 2201 if action == 'swap': 2202 poller = client.web_apps.begin_swap_slot(resource_group_name, webapp, slot, slot_swap_entity) 2203 return poller 2204 if action == 'preview': 2205 if slot is None: 2206 result = client.web_apps.apply_slot_config_to_production(resource_group_name, webapp, slot_swap_entity) 2207 else: 2208 result = client.web_apps.apply_slot_configuration_slot(resource_group_name, webapp, slot, slot_swap_entity) 2209 return result 2210 # we will reset both source slot and target slot 2211 if target_slot is None: 2212 client.web_apps.reset_production_slot_config(resource_group_name, webapp) 2213 else: 2214 client.web_apps.reset_slot_configuration_slot(resource_group_name, webapp, target_slot) 2215 return None 2216 2217 2218def delete_slot(cmd, resource_group_name, webapp, slot): 2219 client = web_client_factory(cmd.cli_ctx) 2220 # TODO: once swagger finalized, expose other parameters like: delete_all_slots, etc... 2221 client.web_apps.delete_slot(resource_group_name, webapp, slot) 2222 2223 2224def set_traffic_routing(cmd, resource_group_name, name, distribution): 2225 RampUpRule = cmd.get_models('RampUpRule') 2226 client = web_client_factory(cmd.cli_ctx) 2227 site = client.web_apps.get(resource_group_name, name) 2228 if not site: 2229 raise CLIError("'{}' app doesn't exist".format(name)) 2230 configs = get_site_configs(cmd, resource_group_name, name) 2231 host_name_split = site.default_host_name.split('.', 1) 2232 host_name_suffix = '.' + host_name_split[1] 2233 host_name_val = host_name_split[0] 2234 configs.experiments.ramp_up_rules = [] 2235 for r in distribution: 2236 slot, percentage = r.split('=') 2237 action_host_name_slot = host_name_val + "-" + slot 2238 configs.experiments.ramp_up_rules.append(RampUpRule(action_host_name=action_host_name_slot + host_name_suffix, 2239 reroute_percentage=float(percentage), 2240 name=slot)) 2241 _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'update_configuration', None, configs) 2242 2243 return configs.experiments.ramp_up_rules 2244 2245 2246def show_traffic_routing(cmd, resource_group_name, name): 2247 configs = get_site_configs(cmd, resource_group_name, name) 2248 return configs.experiments.ramp_up_rules 2249 2250 2251def clear_traffic_routing(cmd, resource_group_name, name): 2252 set_traffic_routing(cmd, resource_group_name, name, []) 2253 2254 2255def add_cors(cmd, resource_group_name, name, allowed_origins, slot=None): 2256 from azure.mgmt.web.models import CorsSettings 2257 configs = get_site_configs(cmd, resource_group_name, name, slot) 2258 if not configs.cors: 2259 configs.cors = CorsSettings() 2260 configs.cors.allowed_origins = (configs.cors.allowed_origins or []) + allowed_origins 2261 result = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'update_configuration', slot, configs) 2262 return result.cors 2263 2264 2265def remove_cors(cmd, resource_group_name, name, allowed_origins, slot=None): 2266 configs = get_site_configs(cmd, resource_group_name, name, slot) 2267 if configs.cors: 2268 if allowed_origins: 2269 configs.cors.allowed_origins = [x for x in (configs.cors.allowed_origins or []) if x not in allowed_origins] 2270 else: 2271 configs.cors.allowed_origins = [] 2272 configs = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'update_configuration', slot, configs) 2273 return configs.cors 2274 2275 2276def show_cors(cmd, resource_group_name, name, slot=None): 2277 configs = get_site_configs(cmd, resource_group_name, name, slot) 2278 return configs.cors 2279 2280 2281def get_streaming_log(cmd, resource_group_name, name, provider=None, slot=None): 2282 scm_url = _get_scm_url(cmd, resource_group_name, name, slot) 2283 streaming_url = scm_url + '/logstream' 2284 if provider: 2285 streaming_url += ('/' + provider.lstrip('/')) 2286 2287 user, password = _get_site_credential(cmd.cli_ctx, resource_group_name, name, slot) 2288 t = threading.Thread(target=_get_log, args=(streaming_url, user, password)) 2289 t.daemon = True 2290 t.start() 2291 2292 while True: 2293 time.sleep(100) # so that ctrl+c can stop the command 2294 2295 2296def download_historical_logs(cmd, resource_group_name, name, log_file=None, slot=None): 2297 scm_url = _get_scm_url(cmd, resource_group_name, name, slot) 2298 url = scm_url.rstrip('/') + '/dump' 2299 user_name, password = _get_site_credential(cmd.cli_ctx, resource_group_name, name, slot) 2300 _get_log(url, user_name, password, log_file) 2301 logger.warning('Downloaded logs to %s', log_file) 2302 2303 2304def _get_site_credential(cli_ctx, resource_group_name, name, slot=None): 2305 creds = _generic_site_operation(cli_ctx, resource_group_name, name, 'begin_list_publishing_credentials', slot) 2306 creds = creds.result() 2307 return (creds.publishing_user_name, creds.publishing_password) 2308 2309 2310def _get_log(url, user_name, password, log_file=None): 2311 import certifi 2312 import urllib3 2313 try: 2314 import urllib3.contrib.pyopenssl 2315 urllib3.contrib.pyopenssl.inject_into_urllib3() 2316 except ImportError: 2317 pass 2318 2319 http = urllib3.PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where()) 2320 headers = urllib3.util.make_headers(basic_auth='{0}:{1}'.format(user_name, password)) 2321 r = http.request( 2322 'GET', 2323 url, 2324 headers=headers, 2325 preload_content=False 2326 ) 2327 if r.status != 200: 2328 raise CLIError("Failed to connect to '{}' with status code '{}' and reason '{}'".format( 2329 url, r.status, r.reason)) 2330 if log_file: # download logs 2331 with open(log_file, 'wb') as f: 2332 while True: 2333 data = r.read(1024) 2334 if not data: 2335 break 2336 f.write(data) 2337 else: # streaming 2338 std_encoding = sys.stdout.encoding 2339 for chunk in r.stream(): 2340 if chunk: 2341 # Extra encode() and decode for stdout which does not surpport 'utf-8' 2342 logger.warning(chunk.decode(encoding='utf-8', errors='replace') 2343 .encode(std_encoding, errors='replace') 2344 .decode(std_encoding, errors='replace') 2345 .rstrip('\n\r')) # each line of log has CRLF. 2346 r.release_conn() 2347 2348 2349def upload_ssl_cert(cmd, resource_group_name, name, certificate_password, certificate_file, slot=None): 2350 Certificate = cmd.get_models('Certificate') 2351 client = web_client_factory(cmd.cli_ctx) 2352 webapp = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot) 2353 cert_file = open(certificate_file, 'rb') 2354 cert_contents = cert_file.read() 2355 hosting_environment_profile_param = (webapp.hosting_environment_profile.name 2356 if webapp.hosting_environment_profile else '') 2357 2358 thumb_print = _get_cert(certificate_password, certificate_file) 2359 cert_name = _generate_cert_name(thumb_print, hosting_environment_profile_param, 2360 webapp.location, resource_group_name) 2361 cert = Certificate(password=certificate_password, pfx_blob=cert_contents, 2362 location=webapp.location, server_farm_id=webapp.server_farm_id) 2363 return client.certificates.create_or_update(resource_group_name, cert_name, cert) 2364 2365 2366def _generate_cert_name(thumb_print, hosting_environment, location, resource_group_name): 2367 return "%s_%s_%s_%s" % (thumb_print, hosting_environment, location, resource_group_name) 2368 2369 2370def _get_cert(certificate_password, certificate_file): 2371 ''' Decrypts the .pfx file ''' 2372 p12 = OpenSSL.crypto.load_pkcs12(open(certificate_file, 'rb').read(), certificate_password) 2373 cert = p12.get_certificate() 2374 digest_algorithm = 'sha1' 2375 thumbprint = cert.digest(digest_algorithm).decode("utf-8").replace(':', '') 2376 return thumbprint 2377 2378 2379def list_ssl_certs(cmd, resource_group_name): 2380 client = web_client_factory(cmd.cli_ctx) 2381 return client.certificates.list_by_resource_group(resource_group_name) 2382 2383 2384def show_ssl_cert(cmd, resource_group_name, certificate_name): 2385 client = web_client_factory(cmd.cli_ctx) 2386 return client.certificates.get(resource_group_name, certificate_name) 2387 2388 2389def delete_ssl_cert(cmd, resource_group_name, certificate_thumbprint): 2390 client = web_client_factory(cmd.cli_ctx) 2391 webapp_certs = client.certificates.list_by_resource_group(resource_group_name) 2392 for webapp_cert in webapp_certs: 2393 if webapp_cert.thumbprint == certificate_thumbprint: 2394 return client.certificates.delete(resource_group_name, webapp_cert.name) 2395 raise CLIError("Certificate for thumbprint '{}' not found".format(certificate_thumbprint)) 2396 2397 2398def import_ssl_cert(cmd, resource_group_name, name, key_vault, key_vault_certificate_name): 2399 Certificate = cmd.get_models('Certificate') 2400 client = web_client_factory(cmd.cli_ctx) 2401 webapp = client.web_apps.get(resource_group_name, name) 2402 if not webapp: 2403 raise CLIError("'{}' app doesn't exist in resource group {}".format(name, resource_group_name)) 2404 server_farm_id = webapp.server_farm_id 2405 location = webapp.location 2406 kv_id = None 2407 if not is_valid_resource_id(key_vault): 2408 kv_client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_KEYVAULT) 2409 key_vaults = kv_client.vaults.list_by_subscription() 2410 for kv in key_vaults: 2411 if key_vault == kv.name: 2412 kv_id = kv.id 2413 break 2414 else: 2415 kv_id = key_vault 2416 2417 if kv_id is None: 2418 kv_msg = 'The Key Vault {0} was not found in the subscription in context. ' \ 2419 'If your Key Vault is in a different subscription, please specify the full Resource ID: ' \ 2420 '\naz .. ssl import -n {1} -g {2} --key-vault-certificate-name {3} ' \ 2421 '--key-vault /subscriptions/[sub id]/resourceGroups/[rg]/providers/Microsoft.KeyVault/' \ 2422 'vaults/{0}'.format(key_vault, name, resource_group_name, key_vault_certificate_name) 2423 logger.warning(kv_msg) 2424 return 2425 2426 kv_id_parts = parse_resource_id(kv_id) 2427 kv_name = kv_id_parts['name'] 2428 kv_resource_group_name = kv_id_parts['resource_group'] 2429 kv_subscription = kv_id_parts['subscription'] 2430 2431 # If in the public cloud, check if certificate is an app service certificate, in the same or a diferent 2432 # subscription 2433 kv_secret_name = None 2434 cloud_type = cmd.cli_ctx.cloud.name 2435 from azure.cli.core.commands.client_factory import get_subscription_id 2436 subscription_id = get_subscription_id(cmd.cli_ctx) 2437 if cloud_type.lower() == PUBLIC_CLOUD.lower(): 2438 if kv_subscription.lower() != subscription_id.lower(): 2439 diff_subscription_client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_APPSERVICE, 2440 subscription_id=kv_subscription) 2441 ascs = diff_subscription_client.app_service_certificate_orders.list() 2442 else: 2443 ascs = client.app_service_certificate_orders.list() 2444 2445 kv_secret_name = None 2446 for asc in ascs: 2447 if asc.name == key_vault_certificate_name: 2448 kv_secret_name = asc.certificates[key_vault_certificate_name].key_vault_secret_name 2449 2450 # if kv_secret_name is not populated, it is not an appservice certificate, proceed for KV certificates 2451 if not kv_secret_name: 2452 kv_secret_name = key_vault_certificate_name 2453 2454 cert_name = '{}-{}-{}'.format(resource_group_name, kv_name, key_vault_certificate_name) 2455 lnk = 'https://azure.github.io/AppService/2016/05/24/Deploying-Azure-Web-App-Certificate-through-Key-Vault.html' 2456 lnk_msg = 'Find more details here: {}'.format(lnk) 2457 if not _check_service_principal_permissions(cmd, kv_resource_group_name, kv_name, kv_subscription): 2458 logger.warning('Unable to verify Key Vault permissions.') 2459 logger.warning('You may need to grant Microsoft.Azure.WebSites service principal the Secret:Get permission') 2460 logger.warning(lnk_msg) 2461 2462 kv_cert_def = Certificate(location=location, key_vault_id=kv_id, password='', 2463 key_vault_secret_name=kv_secret_name, server_farm_id=server_farm_id) 2464 2465 return client.certificates.create_or_update(name=cert_name, resource_group_name=resource_group_name, 2466 certificate_envelope=kv_cert_def) 2467 2468 2469def create_managed_ssl_cert(cmd, resource_group_name, name, hostname, slot=None): 2470 Certificate = cmd.get_models('Certificate') 2471 hostname = hostname.lower() 2472 client = web_client_factory(cmd.cli_ctx) 2473 webapp = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot) 2474 if not webapp: 2475 slot_text = "Deployment slot {} in ".format(slot) if slot else '' 2476 raise CLIError("{0}app {1} doesn't exist in resource group {2}".format(slot_text, name, resource_group_name)) 2477 2478 parsed_plan_id = parse_resource_id(webapp.server_farm_id) 2479 plan_info = client.app_service_plans.get(parsed_plan_id['resource_group'], parsed_plan_id['name']) 2480 if plan_info.sku.tier.upper() == 'FREE' or plan_info.sku.tier.upper() == 'SHARED': 2481 raise CLIError('Managed Certificate is not supported on Free and Shared tier.') 2482 2483 if not _verify_hostname_binding(cmd, resource_group_name, name, hostname, slot): 2484 slot_text = " --slot {}".format(slot) if slot else "" 2485 raise CLIError("Hostname (custom domain) '{0}' is not registered with {1}. " 2486 "Use 'az webapp config hostname add --resource-group {2} " 2487 "--webapp-name {1}{3} --hostname {0}' " 2488 "to register the hostname.".format(hostname, name, resource_group_name, slot_text)) 2489 2490 server_farm_id = webapp.server_farm_id 2491 location = webapp.location 2492 easy_cert_def = Certificate(location=location, canonical_name=hostname, 2493 server_farm_id=server_farm_id, password='') 2494 2495 # TODO: Update manual polling to use LongRunningOperation once backend API & new SDK supports polling 2496 try: 2497 return client.certificates.create_or_update(name=hostname, resource_group_name=resource_group_name, 2498 certificate_envelope=easy_cert_def) 2499 except Exception as ex: 2500 poll_url = ex.response.headers['Location'] if 'Location' in ex.response.headers else None 2501 if ex.response.status_code == 202 and poll_url: 2502 r = send_raw_request(cmd.cli_ctx, method='get', url=poll_url) 2503 poll_timeout = time.time() + 60 * 2 # 2 minute timeout 2504 2505 while r.status_code != 200 and time.time() < poll_timeout: 2506 time.sleep(5) 2507 r = send_raw_request(cmd.cli_ctx, method='get', url=poll_url) 2508 2509 if r.status_code == 200: 2510 try: 2511 return r.json() 2512 except ValueError: 2513 return r.text 2514 logger.warning("Managed Certificate creation in progress. Please use the command " 2515 "'az webapp config ssl show -g %s --certificate-name %s' " 2516 " to view your certificate once it is created", resource_group_name, hostname) 2517 return 2518 raise CLIError(ex) 2519 2520 2521def _check_service_principal_permissions(cmd, resource_group_name, key_vault_name, key_vault_subscription): 2522 from azure.cli.command_modules.role._client_factory import _graph_client_factory 2523 from azure.graphrbac.models import GraphErrorException 2524 from azure.cli.core.commands.client_factory import get_subscription_id 2525 subscription = get_subscription_id(cmd.cli_ctx) 2526 # Cannot check if key vault is in another subscription 2527 if subscription != key_vault_subscription: 2528 return False 2529 kv_client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_KEYVAULT) 2530 vault = kv_client.vaults.get(resource_group_name=resource_group_name, vault_name=key_vault_name) 2531 # Check for Microsoft.Azure.WebSites app registration 2532 AZURE_PUBLIC_WEBSITES_APP_ID = 'abfa0a7c-a6b6-4736-8310-5855508787cd' 2533 AZURE_GOV_WEBSITES_APP_ID = '6a02c803-dafd-4136-b4c3-5a6f318b4714' 2534 graph_sp_client = _graph_client_factory(cmd.cli_ctx).service_principals 2535 for policy in vault.properties.access_policies: 2536 try: 2537 sp = graph_sp_client.get(policy.object_id) 2538 if sp.app_id == AZURE_PUBLIC_WEBSITES_APP_ID or sp.app_id == AZURE_GOV_WEBSITES_APP_ID: 2539 for perm in policy.permissions.secrets: 2540 if perm == "Get": 2541 return True 2542 except GraphErrorException: 2543 pass # Lookup will fail for non service principals (users, groups, etc.) 2544 return False 2545 2546 2547def _update_host_name_ssl_state(cmd, resource_group_name, webapp_name, webapp, 2548 host_name, ssl_state, thumbprint, slot=None): 2549 Site, HostNameSslState = cmd.get_models('Site', 'HostNameSslState') 2550 updated_webapp = Site(host_name_ssl_states=[HostNameSslState(name=host_name, 2551 ssl_state=ssl_state, 2552 thumbprint=thumbprint, 2553 to_update=True)], 2554 location=webapp.location, tags=webapp.tags) 2555 return _generic_site_operation(cmd.cli_ctx, resource_group_name, webapp_name, 'begin_create_or_update', 2556 slot, updated_webapp) 2557 2558 2559def _update_ssl_binding(cmd, resource_group_name, name, certificate_thumbprint, ssl_type, slot=None): 2560 client = web_client_factory(cmd.cli_ctx) 2561 webapp = client.web_apps.get(resource_group_name, name) 2562 if not webapp: 2563 raise ResourceNotFoundError("'{}' app doesn't exist".format(name)) 2564 2565 cert_resource_group_name = parse_resource_id(webapp.server_farm_id)['resource_group'] 2566 webapp_certs = client.certificates.list_by_resource_group(cert_resource_group_name) 2567 2568 found_cert = None 2569 for webapp_cert in webapp_certs: 2570 if webapp_cert.thumbprint == certificate_thumbprint: 2571 found_cert = webapp_cert 2572 if not found_cert: 2573 webapp_certs = client.certificates.list_by_resource_group(resource_group_name) 2574 for webapp_cert in webapp_certs: 2575 if webapp_cert.thumbprint == certificate_thumbprint: 2576 found_cert = webapp_cert 2577 if found_cert: 2578 if len(found_cert.host_names) == 1 and not found_cert.host_names[0].startswith('*'): 2579 return _update_host_name_ssl_state(cmd, resource_group_name, name, webapp, 2580 found_cert.host_names[0], ssl_type, 2581 certificate_thumbprint, slot) 2582 2583 query_result = list_hostnames(cmd, resource_group_name, name, slot) 2584 hostnames_in_webapp = [x.name.split('/')[-1] for x in query_result] 2585 to_update = _match_host_names_from_cert(found_cert.host_names, hostnames_in_webapp) 2586 for h in to_update: 2587 _update_host_name_ssl_state(cmd, resource_group_name, name, webapp, 2588 h, ssl_type, certificate_thumbprint, slot) 2589 2590 return show_webapp(cmd, resource_group_name, name, slot) 2591 2592 raise ResourceNotFoundError("Certificate for thumbprint '{}' not found.".format(certificate_thumbprint)) 2593 2594 2595def bind_ssl_cert(cmd, resource_group_name, name, certificate_thumbprint, ssl_type, slot=None): 2596 SslState = cmd.get_models('SslState') 2597 return _update_ssl_binding(cmd, resource_group_name, name, certificate_thumbprint, 2598 SslState.sni_enabled if ssl_type == 'SNI' else SslState.ip_based_enabled, slot) 2599 2600 2601def unbind_ssl_cert(cmd, resource_group_name, name, certificate_thumbprint, slot=None): 2602 SslState = cmd.get_models('SslState') 2603 return _update_ssl_binding(cmd, resource_group_name, name, 2604 certificate_thumbprint, SslState.disabled, slot) 2605 2606 2607def _match_host_names_from_cert(hostnames_from_cert, hostnames_in_webapp): 2608 # the goal is to match '*.foo.com' with host name like 'admin.foo.com', 'logs.foo.com', etc 2609 matched = set() 2610 for hostname in hostnames_from_cert: 2611 if hostname.startswith('*'): 2612 for h in hostnames_in_webapp: 2613 if hostname[hostname.find('.'):] == h[h.find('.'):]: 2614 matched.add(h) 2615 elif hostname in hostnames_in_webapp: 2616 matched.add(hostname) 2617 return matched 2618 2619 2620# help class handles runtime stack in format like 'node|6.1', 'php|5.5' 2621class _StackRuntimeHelper: 2622 2623 def __init__(self, cmd, client, linux=False): 2624 self._cmd = cmd 2625 self._client = client 2626 self._linux = linux 2627 self._stacks = [] 2628 2629 @staticmethod 2630 def remove_delimiters(runtime): 2631 import re 2632 # delimiters allowed: '|', ':' 2633 if '|' in runtime: 2634 runtime = re.split('[|]', runtime) 2635 elif ':' in runtime: 2636 runtime = re.split('[:]', runtime) 2637 else: 2638 runtime = [runtime] 2639 return '|'.join(filter(None, runtime)) 2640 2641 def resolve(self, display_name): 2642 self._load_stacks_hardcoded() 2643 return next((s for s in self._stacks if s['displayName'].lower() == display_name.lower()), 2644 None) 2645 2646 @property 2647 def stacks(self): 2648 self._load_stacks_hardcoded() 2649 return self._stacks 2650 2651 @staticmethod 2652 def update_site_config(stack, site_config, cmd=None): 2653 for k, v in stack['configs'].items(): 2654 setattr(site_config, k, v) 2655 return site_config 2656 2657 @staticmethod 2658 def update_site_appsettings(cmd, stack, site_config): 2659 NameValuePair = cmd.get_models('NameValuePair') 2660 if site_config.app_settings is None: 2661 site_config.app_settings = [] 2662 2663 for k, v in stack['configs'].items(): 2664 already_in_appsettings = False 2665 for app_setting in site_config.app_settings: 2666 if app_setting.name == k: 2667 already_in_appsettings = True 2668 app_setting.value = v 2669 if not already_in_appsettings: 2670 site_config.app_settings.append(NameValuePair(name=k, value=v)) 2671 return site_config 2672 2673 def _load_stacks_hardcoded(self): 2674 if self._stacks: 2675 return 2676 result = [] 2677 if self._linux: 2678 result = get_file_json(RUNTIME_STACKS)['linux'] 2679 for r in result: 2680 r['setter'] = _StackRuntimeHelper.update_site_config 2681 else: # Windows stacks 2682 result = get_file_json(RUNTIME_STACKS)['windows'] 2683 for r in result: 2684 r['setter'] = (_StackRuntimeHelper.update_site_appsettings if 'node' in 2685 r['displayName'] else _StackRuntimeHelper.update_site_config) 2686 self._stacks = result 2687 2688 # Currently using hardcoded values instead of this function. This function calls the stacks API; 2689 # Stacks API is updated with Antares deployments, 2690 # which are infrequent and don't line up with stacks EOL schedule. 2691 def _load_stacks(self): 2692 if self._stacks: 2693 return 2694 os_type = ('Linux' if self._linux else 'Windows') 2695 raw_stacks = self._client.provider.get_available_stacks(os_type_selected=os_type, raw=True) 2696 bytes_value = raw_stacks._get_next().content # pylint: disable=protected-access 2697 json_value = bytes_value.decode('utf8') 2698 json_stacks = json.loads(json_value) 2699 stacks = json_stacks['value'] 2700 result = [] 2701 if self._linux: 2702 for properties in [(s['properties']) for s in stacks]: 2703 for major in properties['majorVersions']: 2704 default_minor = next((m for m in (major['minorVersions'] or []) if m['isDefault']), 2705 None) 2706 result.append({ 2707 'displayName': (default_minor['runtimeVersion'] 2708 if default_minor else major['runtimeVersion']) 2709 }) 2710 else: # Windows stacks 2711 config_mappings = { 2712 'node': 'WEBSITE_NODE_DEFAULT_VERSION', 2713 'python': 'python_version', 2714 'php': 'php_version', 2715 'aspnet': 'net_framework_version' 2716 } 2717 2718 # get all stack version except 'java' 2719 for stack in stacks: 2720 if stack['name'] not in config_mappings: 2721 continue 2722 name, properties = stack['name'], stack['properties'] 2723 for major in properties['majorVersions']: 2724 default_minor = next((m for m in (major['minorVersions'] or []) if m['isDefault']), 2725 None) 2726 result.append({ 2727 'displayName': name + '|' + major['displayVersion'], 2728 'configs': { 2729 config_mappings[name]: (default_minor['runtimeVersion'] 2730 if default_minor else major['runtimeVersion']) 2731 } 2732 }) 2733 2734 # deal with java, which pairs with java container version 2735 java_stack = next((s for s in stacks if s['name'] == 'java')) 2736 java_container_stack = next((s for s in stacks if s['name'] == 'javaContainers')) 2737 for java_version in java_stack['properties']['majorVersions']: 2738 for fx in java_container_stack['properties']['frameworks']: 2739 for fx_version in fx['majorVersions']: 2740 result.append({ 2741 'displayName': 'java|{}|{}|{}'.format(java_version['displayVersion'], 2742 fx['display'], 2743 fx_version['displayVersion']), 2744 'configs': { 2745 'java_version': java_version['runtimeVersion'], 2746 'java_container': fx['name'], 2747 'java_container_version': fx_version['runtimeVersion'] 2748 } 2749 }) 2750 2751 for r in result: 2752 r['setter'] = (_StackRuntimeHelper.update_site_appsettings if 'node' in 2753 r['displayName'] else _StackRuntimeHelper.update_site_config) 2754 self._stacks = result 2755 2756 2757def get_app_insights_key(cli_ctx, resource_group, name): 2758 appinsights_client = get_mgmt_service_client(cli_ctx, ApplicationInsightsManagementClient) 2759 appinsights = appinsights_client.components.get(resource_group, name) 2760 if appinsights is None or appinsights.instrumentation_key is None: 2761 raise CLIError("App Insights {} under resource group {} was not found.".format(name, resource_group)) 2762 return appinsights.instrumentation_key 2763 2764 2765def create_functionapp_app_service_plan(cmd, resource_group_name, name, is_linux, sku, 2766 number_of_workers=None, max_burst=None, location=None, tags=None): 2767 SkuDescription, AppServicePlan = cmd.get_models('SkuDescription', 'AppServicePlan') 2768 sku = _normalize_sku(sku) 2769 tier = get_sku_name(sku) 2770 if max_burst is not None: 2771 if tier.lower() != "elasticpremium": 2772 raise CLIError("Usage error: --max-burst is only supported for Elastic Premium (EP) plans") 2773 max_burst = validate_range_of_int_flag('--max-burst', max_burst, min_val=0, max_val=20) 2774 if number_of_workers is not None: 2775 number_of_workers = validate_range_of_int_flag('--number-of-workers / --min-elastic-worker-count', 2776 number_of_workers, min_val=0, max_val=20) 2777 client = web_client_factory(cmd.cli_ctx) 2778 if location is None: 2779 location = _get_location_from_resource_group(cmd.cli_ctx, resource_group_name) 2780 sku_def = SkuDescription(tier=tier, name=sku, capacity=number_of_workers) 2781 plan_def = AppServicePlan(location=location, tags=tags, sku=sku_def, 2782 reserved=(is_linux or None), maximum_elastic_worker_count=max_burst, 2783 hyper_v=None, name=name) 2784 return client.app_service_plans.begin_create_or_update(resource_group_name, name, plan_def) 2785 2786 2787def is_plan_consumption(cmd, plan_info): 2788 SkuDescription, AppServicePlan = cmd.get_models('SkuDescription', 'AppServicePlan') 2789 if isinstance(plan_info, AppServicePlan): 2790 if isinstance(plan_info.sku, SkuDescription): 2791 return plan_info.sku.tier.lower() == 'dynamic' 2792 return False 2793 2794 2795def is_plan_elastic_premium(cmd, plan_info): 2796 SkuDescription, AppServicePlan = cmd.get_models('SkuDescription', 'AppServicePlan') 2797 if isinstance(plan_info, AppServicePlan): 2798 if isinstance(plan_info.sku, SkuDescription): 2799 return plan_info.sku.tier == 'ElasticPremium' 2800 return False 2801 2802 2803def validate_and_convert_to_int(flag, val): 2804 try: 2805 return int(val) 2806 except ValueError: 2807 raise CLIError("Usage error: {} is expected to have an int value.".format(flag)) 2808 2809 2810def validate_range_of_int_flag(flag_name, value, min_val, max_val): 2811 value = validate_and_convert_to_int(flag_name, value) 2812 if min_val > value or value > max_val: 2813 raise CLIError("Usage error: {} is expected to be between {} and {} (inclusive)".format(flag_name, min_val, 2814 max_val)) 2815 return value 2816 2817 2818def create_function(cmd, resource_group_name, name, storage_account, plan=None, 2819 os_type=None, functions_version=None, runtime=None, runtime_version=None, 2820 consumption_plan_location=None, app_insights=None, app_insights_key=None, 2821 disable_app_insights=None, deployment_source_url=None, 2822 deployment_source_branch='master', deployment_local_git=None, 2823 docker_registry_server_password=None, docker_registry_server_user=None, 2824 deployment_container_image_name=None, tags=None, assign_identities=None, 2825 role='Contributor', scope=None): 2826 # pylint: disable=too-many-statements, too-many-branches 2827 if functions_version is None: 2828 logger.warning("No functions version specified so defaulting to 2. In the future, specifying a version will " 2829 "be required. To create a 2.x function you would pass in the flag `--functions-version 2`") 2830 functions_version = '2' 2831 if deployment_source_url and deployment_local_git: 2832 raise CLIError('usage error: --deployment-source-url <url> | --deployment-local-git') 2833 if bool(plan) == bool(consumption_plan_location): 2834 raise CLIError("usage error: --plan NAME_OR_ID | --consumption-plan-location LOCATION") 2835 SiteConfig, Site, NameValuePair = cmd.get_models('SiteConfig', 'Site', 'NameValuePair') 2836 docker_registry_server_url = parse_docker_image_name(deployment_container_image_name) 2837 disable_app_insights = (disable_app_insights == "true") 2838 2839 site_config = SiteConfig(app_settings=[]) 2840 functionapp_def = Site(location=None, site_config=site_config, tags=tags) 2841 KEYS = FUNCTIONS_STACKS_API_KEYS() 2842 client = web_client_factory(cmd.cli_ctx) 2843 plan_info = None 2844 if runtime is not None: 2845 runtime = runtime.lower() 2846 2847 if consumption_plan_location: 2848 locations = list_consumption_locations(cmd) 2849 location = next((loc for loc in locations if loc['name'].lower() == consumption_plan_location.lower()), None) 2850 if location is None: 2851 raise CLIError("Location is invalid. Use: az functionapp list-consumption-locations") 2852 functionapp_def.location = consumption_plan_location 2853 functionapp_def.kind = 'functionapp' 2854 # if os_type is None, the os type is windows 2855 is_linux = os_type and os_type.lower() == 'linux' 2856 2857 else: # apps with SKU based plan 2858 if is_valid_resource_id(plan): 2859 parse_result = parse_resource_id(plan) 2860 plan_info = client.app_service_plans.get(parse_result['resource_group'], parse_result['name']) 2861 else: 2862 plan_info = client.app_service_plans.get(resource_group_name, plan) 2863 if not plan_info: 2864 raise CLIError("The plan '{}' doesn't exist".format(plan)) 2865 location = plan_info.location 2866 is_linux = plan_info.reserved 2867 functionapp_def.server_farm_id = plan 2868 functionapp_def.location = location 2869 2870 if functions_version == '2' and functionapp_def.location in FUNCTIONS_NO_V2_REGIONS: 2871 raise CLIError("2.x functions are not supported in this region. To create a 3.x function, " 2872 "pass in the flag '--functions-version 3'") 2873 2874 if is_linux and not runtime and (consumption_plan_location or not deployment_container_image_name): 2875 raise CLIError( 2876 "usage error: --runtime RUNTIME required for linux functions apps without custom image.") 2877 2878 runtime_stacks_json = _load_runtime_stacks_json_functionapp(is_linux) 2879 2880 if runtime is None and runtime_version is not None: 2881 raise CLIError('Must specify --runtime to use --runtime-version') 2882 2883 # get the matching runtime stack object 2884 runtime_json = _get_matching_runtime_json_functionapp(runtime_stacks_json, runtime if runtime else 'dotnet') 2885 if not runtime_json: 2886 # no matching runtime for os 2887 os_string = "linux" if is_linux else "windows" 2888 supported_runtimes = list(map(lambda x: x[KEYS.NAME], runtime_stacks_json)) 2889 raise CLIError("usage error: Currently supported runtimes (--runtime) in {} function apps are: {}." 2890 .format(os_string, ', '.join(supported_runtimes))) 2891 2892 runtime_version_json = _get_matching_runtime_version_json_functionapp(runtime_json, 2893 functions_version, 2894 runtime_version, 2895 is_linux) 2896 if not runtime_version_json: 2897 supported_runtime_versions = list(map(lambda x: x[KEYS.DISPLAY_VERSION], 2898 _get_supported_runtime_versions_functionapp(runtime_json, 2899 functions_version))) 2900 if runtime_version: 2901 if runtime == 'dotnet': 2902 raise CLIError('--runtime-version is not supported for --runtime dotnet. Dotnet version is determined ' 2903 'by --functions-version. Dotnet version {} is not supported by Functions version {}.' 2904 .format(runtime_version, functions_version)) 2905 raise CLIError('--runtime-version {} is not supported for the selected --runtime {} and ' 2906 '--functions-version {}. Supported versions are: {}.' 2907 .format(runtime_version, 2908 runtime, 2909 functions_version, 2910 ', '.join(supported_runtime_versions))) 2911 2912 # if runtime_version was not specified, then that runtime is not supported for that functions version 2913 raise CLIError('no supported --runtime-version found for the selected --runtime {} and ' 2914 '--functions-version {}' 2915 .format(runtime, functions_version)) 2916 2917 if runtime == 'dotnet': 2918 logger.warning('--runtime-version is not supported for --runtime dotnet. Dotnet version is determined by ' 2919 '--functions-version. Dotnet version will be %s for this function app.', 2920 runtime_version_json[KEYS.DISPLAY_VERSION]) 2921 2922 if runtime_version_json[KEYS.IS_DEPRECATED]: 2923 logger.warning('%s version %s has been deprecated. In the future, this version will be unavailable. ' 2924 'Please update your command to use a more recent version. For a list of supported ' 2925 '--runtime-versions, run \"az functionapp create -h\"', 2926 runtime_json[KEYS.PROPERTIES][KEYS.DISPLAY], runtime_version_json[KEYS.DISPLAY_VERSION]) 2927 2928 site_config_json = runtime_version_json[KEYS.SITE_CONFIG_DICT] 2929 app_settings_json = runtime_version_json[KEYS.APP_SETTINGS_DICT] 2930 2931 con_string = _validate_and_get_connection_string(cmd.cli_ctx, resource_group_name, storage_account) 2932 2933 if is_linux: 2934 functionapp_def.kind = 'functionapp,linux' 2935 functionapp_def.reserved = True 2936 is_consumption = consumption_plan_location is not None 2937 if not is_consumption: 2938 site_config.app_settings.append(NameValuePair(name='MACHINEKEY_DecryptionKey', 2939 value=str(hexlify(urandom(32)).decode()).upper())) 2940 if deployment_container_image_name: 2941 functionapp_def.kind = 'functionapp,linux,container' 2942 site_config.app_settings.append(NameValuePair(name='DOCKER_CUSTOM_IMAGE_NAME', 2943 value=deployment_container_image_name)) 2944 site_config.app_settings.append(NameValuePair(name='FUNCTION_APP_EDIT_MODE', value='readOnly')) 2945 site_config.app_settings.append(NameValuePair(name='WEBSITES_ENABLE_APP_SERVICE_STORAGE', 2946 value='false')) 2947 site_config.linux_fx_version = _format_fx_version(deployment_container_image_name) 2948 2949 # clear all runtime specific configs and settings 2950 site_config_json = {KEYS.USE_32_BIT_WORKER_PROC: False} 2951 app_settings_json = {} 2952 2953 # ensure that app insights is created if not disabled 2954 runtime_version_json[KEYS.APPLICATION_INSIGHTS] = True 2955 else: 2956 site_config.app_settings.append(NameValuePair(name='WEBSITES_ENABLE_APP_SERVICE_STORAGE', 2957 value='true')) 2958 else: 2959 functionapp_def.kind = 'functionapp' 2960 2961 # set site configs 2962 for prop, value in site_config_json.items(): 2963 snake_case_prop = _convert_camel_to_snake_case(prop) 2964 setattr(site_config, snake_case_prop, value) 2965 2966 # temporary workaround for dotnet-isolated linux consumption apps 2967 if is_linux and consumption_plan_location is not None and runtime == 'dotnet-isolated': 2968 site_config.linux_fx_version = '' 2969 2970 # adding app settings 2971 for app_setting, value in app_settings_json.items(): 2972 site_config.app_settings.append(NameValuePair(name=app_setting, value=value)) 2973 2974 site_config.app_settings.append(NameValuePair(name='FUNCTIONS_EXTENSION_VERSION', 2975 value=_get_extension_version_functionapp(functions_version))) 2976 site_config.app_settings.append(NameValuePair(name='AzureWebJobsStorage', value=con_string)) 2977 2978 # If plan is not consumption or elastic premium, we need to set always on 2979 if consumption_plan_location is None and not is_plan_elastic_premium(cmd, plan_info): 2980 site_config.always_on = True 2981 2982 # If plan is elastic premium or consumption, we need these app settings 2983 if is_plan_elastic_premium(cmd, plan_info) or consumption_plan_location is not None: 2984 site_config.app_settings.append(NameValuePair(name='WEBSITE_CONTENTAZUREFILECONNECTIONSTRING', 2985 value=con_string)) 2986 site_config.app_settings.append(NameValuePair(name='WEBSITE_CONTENTSHARE', value=_get_content_share_name(name))) 2987 2988 create_app_insights = False 2989 2990 if app_insights_key is not None: 2991 site_config.app_settings.append(NameValuePair(name='APPINSIGHTS_INSTRUMENTATIONKEY', 2992 value=app_insights_key)) 2993 elif app_insights is not None: 2994 instrumentation_key = get_app_insights_key(cmd.cli_ctx, resource_group_name, app_insights) 2995 site_config.app_settings.append(NameValuePair(name='APPINSIGHTS_INSTRUMENTATIONKEY', 2996 value=instrumentation_key)) 2997 elif disable_app_insights or not runtime_version_json[KEYS.APPLICATION_INSIGHTS]: 2998 # set up dashboard if no app insights 2999 site_config.app_settings.append(NameValuePair(name='AzureWebJobsDashboard', value=con_string)) 3000 elif not disable_app_insights and runtime_version_json[KEYS.APPLICATION_INSIGHTS]: 3001 create_app_insights = True 3002 3003 poller = client.web_apps.begin_create_or_update(resource_group_name, name, functionapp_def) 3004 functionapp = LongRunningOperation(cmd.cli_ctx)(poller) 3005 3006 if consumption_plan_location and is_linux: 3007 logger.warning("Your Linux function app '%s', that uses a consumption plan has been successfully " 3008 "created but is not active until content is published using " 3009 "Azure Portal or the Functions Core Tools.", name) 3010 else: 3011 _set_remote_or_local_git(cmd, functionapp, resource_group_name, name, deployment_source_url, 3012 deployment_source_branch, deployment_local_git) 3013 3014 if create_app_insights: 3015 try: 3016 try_create_application_insights(cmd, functionapp) 3017 except Exception: # pylint: disable=broad-except 3018 logger.warning('Error while trying to create and configure an Application Insights for the Function App. ' 3019 'Please use the Azure Portal to create and configure the Application Insights, if needed.') 3020 update_app_settings(cmd, functionapp.resource_group, functionapp.name, 3021 ['AzureWebJobsDashboard={}'.format(con_string)]) 3022 3023 if deployment_container_image_name: 3024 update_container_settings_functionapp(cmd, resource_group_name, name, docker_registry_server_url, 3025 deployment_container_image_name, docker_registry_server_user, 3026 docker_registry_server_password) 3027 3028 if assign_identities is not None: 3029 identity = assign_identity(cmd, resource_group_name, name, assign_identities, 3030 role, None, scope) 3031 functionapp.identity = identity 3032 3033 return functionapp 3034 3035 3036def _load_runtime_stacks_json_functionapp(is_linux): 3037 KEYS = FUNCTIONS_STACKS_API_KEYS() 3038 if is_linux: 3039 return get_file_json(FUNCTIONS_STACKS_API_JSON_PATHS['linux'])[KEYS.VALUE] 3040 return get_file_json(FUNCTIONS_STACKS_API_JSON_PATHS['windows'])[KEYS.VALUE] 3041 3042 3043def _get_matching_runtime_json_functionapp(stacks_json, runtime): 3044 KEYS = FUNCTIONS_STACKS_API_KEYS() 3045 matching_runtime_json = list(filter(lambda x: x[KEYS.NAME] == runtime, stacks_json)) 3046 if matching_runtime_json: 3047 return matching_runtime_json[0] 3048 return None 3049 3050 3051def _get_supported_runtime_versions_functionapp(runtime_json, functions_version): 3052 KEYS = FUNCTIONS_STACKS_API_KEYS() 3053 extension_version = _get_extension_version_functionapp(functions_version) 3054 supported_versions_list = [] 3055 3056 for runtime_version_json in runtime_json[KEYS.PROPERTIES][KEYS.MAJOR_VERSIONS]: 3057 if extension_version in runtime_version_json[KEYS.SUPPORTED_EXTENSION_VERSIONS]: 3058 supported_versions_list.append(runtime_version_json) 3059 return supported_versions_list 3060 3061 3062def _get_matching_runtime_version_json_functionapp(runtime_json, functions_version, runtime_version, is_linux): 3063 KEYS = FUNCTIONS_STACKS_API_KEYS() 3064 extension_version = _get_extension_version_functionapp(functions_version) 3065 if runtime_version: 3066 for runtime_version_json in runtime_json[KEYS.PROPERTIES][KEYS.MAJOR_VERSIONS]: 3067 if (runtime_version_json[KEYS.DISPLAY_VERSION] == runtime_version and 3068 extension_version in runtime_version_json[KEYS.SUPPORTED_EXTENSION_VERSIONS]): 3069 return runtime_version_json 3070 return None 3071 3072 # find the matching default runtime version 3073 supported_versions_list = _get_supported_runtime_versions_functionapp(runtime_json, functions_version) 3074 default_version_json = {} 3075 default_version = 0.0 3076 for current_runtime_version_json in supported_versions_list: 3077 if current_runtime_version_json[KEYS.IS_DEFAULT]: 3078 current_version = _get_runtime_version_functionapp(current_runtime_version_json[KEYS.RUNTIME_VERSION], 3079 is_linux) 3080 if not default_version_json or default_version < current_version: 3081 default_version_json = current_runtime_version_json 3082 default_version = current_version 3083 return default_version_json 3084 3085 3086def _get_extension_version_functionapp(functions_version): 3087 if functions_version is not None: 3088 return '~{}'.format(functions_version) 3089 return '~2' 3090 3091 3092def _get_app_setting_set_functionapp(site_config, app_setting): 3093 return list(filter(lambda x: x.name == app_setting, site_config.app_settings)) 3094 3095 3096def _convert_camel_to_snake_case(text): 3097 return reduce(lambda x, y: x + ('_' if y.isupper() else '') + y, text).lower() 3098 3099 3100def _get_runtime_version_functionapp(version_string, is_linux): 3101 import re 3102 windows_match = re.fullmatch(FUNCTIONS_WINDOWS_RUNTIME_VERSION_REGEX, version_string) 3103 if windows_match: 3104 return float(windows_match.group(1)) 3105 3106 linux_match = re.fullmatch(FUNCTIONS_LINUX_RUNTIME_VERSION_REGEX, version_string) 3107 if linux_match: 3108 return float(linux_match.group(1)) 3109 3110 try: 3111 return float(version_string) 3112 except ValueError: 3113 return 0 3114 3115 3116def _get_content_share_name(app_name): 3117 # content share name should be up to 63 characters long, lowercase letter and digits, and random 3118 # so take the first 50 characters of the app name and add the last 12 digits of a random uuid 3119 share_name = app_name[0:50] 3120 suffix = str(uuid.uuid4()).split('-')[-1] 3121 return share_name.lower() + suffix 3122 3123 3124def try_create_application_insights(cmd, functionapp): 3125 creation_failed_warn = 'Unable to create the Application Insights for the Function App. ' \ 3126 'Please use the Azure Portal to manually create and configure the Application Insights, ' \ 3127 'if needed.' 3128 3129 ai_resource_group_name = functionapp.resource_group 3130 ai_name = functionapp.name 3131 ai_location = functionapp.location 3132 3133 app_insights_client = get_mgmt_service_client(cmd.cli_ctx, ApplicationInsightsManagementClient) 3134 ai_properties = { 3135 "name": ai_name, 3136 "location": ai_location, 3137 "kind": "web", 3138 "properties": { 3139 "Application_Type": "web" 3140 } 3141 } 3142 appinsights = app_insights_client.components.create_or_update(ai_resource_group_name, ai_name, ai_properties) 3143 if appinsights is None or appinsights.instrumentation_key is None: 3144 logger.warning(creation_failed_warn) 3145 return 3146 3147 # We make this success message as a warning to no interfere with regular JSON output in stdout 3148 logger.warning('Application Insights \"%s\" was created for this Function App. ' 3149 'You can visit https://portal.azure.com/#resource%s/overview to view your ' 3150 'Application Insights component', appinsights.name, appinsights.id) 3151 3152 update_app_settings(cmd, functionapp.resource_group, functionapp.name, 3153 ['APPINSIGHTS_INSTRUMENTATIONKEY={}'.format(appinsights.instrumentation_key)]) 3154 3155 3156def _set_remote_or_local_git(cmd, webapp, resource_group_name, name, deployment_source_url=None, 3157 deployment_source_branch='master', deployment_local_git=None): 3158 if deployment_source_url: 3159 logger.warning("Linking to git repository '%s'", deployment_source_url) 3160 try: 3161 config_source_control(cmd, resource_group_name, name, deployment_source_url, 'git', 3162 deployment_source_branch, manual_integration=True) 3163 except Exception as ex: # pylint: disable=broad-except 3164 ex = ex_handler_factory(no_throw=True)(ex) 3165 logger.warning("Link to git repository failed due to error '%s'", ex) 3166 3167 if deployment_local_git: 3168 local_git_info = enable_local_git(cmd, resource_group_name, name) 3169 logger.warning("Local git is configured with url of '%s'", local_git_info['url']) 3170 setattr(webapp, 'deploymentLocalGitUrl', local_git_info['url']) 3171 3172 3173def _validate_and_get_connection_string(cli_ctx, resource_group_name, storage_account): 3174 sa_resource_group = resource_group_name 3175 if is_valid_resource_id(storage_account): 3176 sa_resource_group = parse_resource_id(storage_account)['resource_group'] 3177 storage_account = parse_resource_id(storage_account)['name'] 3178 storage_client = get_mgmt_service_client(cli_ctx, StorageManagementClient) 3179 storage_properties = storage_client.storage_accounts.get_properties(sa_resource_group, 3180 storage_account) 3181 error_message = '' 3182 endpoints = storage_properties.primary_endpoints 3183 sku = storage_properties.sku.name 3184 allowed_storage_types = ['Standard_GRS', 'Standard_RAGRS', 'Standard_LRS', 'Standard_ZRS', 'Premium_LRS'] 3185 3186 for e in ['blob', 'queue', 'table']: 3187 if not getattr(endpoints, e, None): 3188 error_message = "Storage account '{}' has no '{}' endpoint. It must have table, queue, and blob endpoints all enabled".format(storage_account, e) # pylint: disable=line-too-long 3189 if sku not in allowed_storage_types: 3190 error_message += 'Storage type {} is not allowed'.format(sku) 3191 3192 if error_message: 3193 raise CLIError(error_message) 3194 3195 obj = storage_client.storage_accounts.list_keys(sa_resource_group, storage_account) # pylint: disable=no-member 3196 try: 3197 keys = [obj.keys[0].value, obj.keys[1].value] # pylint: disable=no-member 3198 except AttributeError: 3199 # Older API versions have a slightly different structure 3200 keys = [obj.key1, obj.key2] # pylint: disable=no-member 3201 3202 endpoint_suffix = cli_ctx.cloud.suffixes.storage_endpoint 3203 connection_string = 'DefaultEndpointsProtocol={};EndpointSuffix={};AccountName={};AccountKey={}'.format( 3204 "https", 3205 endpoint_suffix, 3206 storage_account, 3207 keys[0]) # pylint: disable=no-member 3208 3209 return connection_string 3210 3211 3212def list_consumption_locations(cmd): 3213 client = web_client_factory(cmd.cli_ctx) 3214 regions = client.list_geo_regions(sku='Dynamic') 3215 return [{'name': x.name.lower().replace(' ', '')} for x in regions] 3216 3217 3218def list_locations(cmd, sku, linux_workers_enabled=None): 3219 web_client = web_client_factory(cmd.cli_ctx) 3220 full_sku = get_sku_name(sku) 3221 web_client_geo_regions = web_client.list_geo_regions(sku=full_sku, linux_workers_enabled=linux_workers_enabled) 3222 3223 providers_client = providers_client_factory(cmd.cli_ctx) 3224 providers_client_locations_list = getattr(providers_client.get('Microsoft.Web'), 'resource_types', []) 3225 for resource_type in providers_client_locations_list: 3226 if resource_type.resource_type == 'sites': 3227 providers_client_locations_list = resource_type.locations 3228 break 3229 3230 return [geo_region for geo_region in web_client_geo_regions if geo_region.name in providers_client_locations_list] 3231 3232 3233def _check_zip_deployment_status(cmd, rg_name, name, deployment_status_url, authorization, timeout=None): 3234 import requests 3235 from azure.cli.core.util import should_disable_connection_verify 3236 total_trials = (int(timeout) // 2) if timeout else 450 3237 num_trials = 0 3238 while num_trials < total_trials: 3239 time.sleep(2) 3240 response = requests.get(deployment_status_url, headers=authorization, 3241 verify=not should_disable_connection_verify()) 3242 try: 3243 res_dict = response.json() 3244 except json.decoder.JSONDecodeError: 3245 logger.warning("Deployment status endpoint %s returns malformed data. Retrying...", deployment_status_url) 3246 res_dict = {} 3247 finally: 3248 num_trials = num_trials + 1 3249 3250 if res_dict.get('status', 0) == 3: 3251 _configure_default_logging(cmd, rg_name, name) 3252 raise CLIError("Zip deployment failed. {}. Please run the command az webapp log deployment show " 3253 "-n {} -g {}".format(res_dict, name, rg_name)) 3254 if res_dict.get('status', 0) == 4: 3255 break 3256 if 'progress' in res_dict: 3257 logger.info(res_dict['progress']) # show only in debug mode, customers seem to find this confusing 3258 # if the deployment is taking longer than expected 3259 if res_dict.get('status', 0) != 4: 3260 _configure_default_logging(cmd, rg_name, name) 3261 raise CLIError("""Timeout reached by the command, however, the deployment operation 3262 is still on-going. Navigate to your scm site to check the deployment status""") 3263 return res_dict 3264 3265 3266def list_continuous_webjobs(cmd, resource_group_name, name, slot=None): 3267 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'list_continuous_web_jobs', slot) 3268 3269 3270def start_continuous_webjob(cmd, resource_group_name, name, webjob_name, slot=None): 3271 client = web_client_factory(cmd.cli_ctx) 3272 if slot: 3273 client.web_apps.start_continuous_web_job_slot(resource_group_name, name, webjob_name, slot) 3274 return client.web_apps.get_continuous_web_job_slot(resource_group_name, name, webjob_name, slot) 3275 client.web_apps.start_continuous_web_job(resource_group_name, name, webjob_name) 3276 return client.web_apps.get_continuous_web_job(resource_group_name, name, webjob_name) 3277 3278 3279def stop_continuous_webjob(cmd, resource_group_name, name, webjob_name, slot=None): 3280 client = web_client_factory(cmd.cli_ctx) 3281 if slot: 3282 client.web_apps.stop_continuous_web_job_slot(resource_group_name, name, webjob_name, slot) 3283 return client.web_apps.get_continuous_web_job_slot(resource_group_name, name, webjob_name, slot) 3284 client.web_apps.stop_continuous_web_job(resource_group_name, name, webjob_name) 3285 return client.web_apps.get_continuous_web_job(resource_group_name, name, webjob_name) 3286 3287 3288def remove_continuous_webjob(cmd, resource_group_name, name, webjob_name, slot=None): 3289 client = web_client_factory(cmd.cli_ctx) 3290 if slot: 3291 return client.web_apps.delete_continuous_web_job_slot(resource_group_name, name, webjob_name, slot) 3292 return client.web_apps.delete_continuous_web_job(resource_group_name, name, webjob_name) 3293 3294 3295def list_triggered_webjobs(cmd, resource_group_name, name, slot=None): 3296 return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'list_triggered_web_jobs', slot) 3297 3298 3299def run_triggered_webjob(cmd, resource_group_name, name, webjob_name, slot=None): 3300 client = web_client_factory(cmd.cli_ctx) 3301 if slot: 3302 client.web_apps.run_triggered_web_job_slot(resource_group_name, name, webjob_name, slot) 3303 return client.web_apps.get_triggered_web_job_slot(resource_group_name, name, webjob_name, slot) 3304 client.web_apps.run_triggered_web_job(resource_group_name, name, webjob_name) 3305 return client.web_apps.get_triggered_web_job(resource_group_name, name, webjob_name) 3306 3307 3308def remove_triggered_webjob(cmd, resource_group_name, name, webjob_name, slot=None): 3309 client = web_client_factory(cmd.cli_ctx) 3310 if slot: 3311 return client.web_apps.delete_triggered_web_job_slot(resource_group_name, name, webjob_name, slot) 3312 return client.web_apps.delete_triggered_web_job(resource_group_name, name, webjob_name) 3313 3314 3315def list_hc(cmd, name, resource_group_name, slot=None): 3316 client = web_client_factory(cmd.cli_ctx) 3317 if slot is None: 3318 listed_vals = client.web_apps.list_hybrid_connections(resource_group_name, name) 3319 else: 3320 listed_vals = client.web_apps.list_hybrid_connections_slot(resource_group_name, name, slot) 3321 3322 # reformats hybrid connection, to prune unnecessary fields 3323 mod_list = [] 3324 for x in listed_vals.additional_properties["value"]: 3325 properties = x["properties"] 3326 resourceGroup = x["id"].split("/") 3327 mod_hc = { 3328 "id": x["id"], 3329 "location": x["location"], 3330 "name": x["name"], 3331 "properties": { 3332 "hostname": properties["hostname"], 3333 "port": properties["port"], 3334 "relayArmUri": properties["relayArmUri"], 3335 "relayName": properties["relayName"], 3336 "serviceBusNamespace": properties["serviceBusNamespace"], 3337 "serviceBusSuffix": properties["serviceBusSuffix"] 3338 }, 3339 "resourceGroup": resourceGroup[4], 3340 "type": x["type"] 3341 } 3342 mod_list.append(mod_hc) 3343 return mod_list 3344 3345 3346def add_hc(cmd, name, resource_group_name, namespace, hybrid_connection, slot=None): 3347 HybridConnection = cmd.get_models('HybridConnection') 3348 3349 web_client = web_client_factory(cmd.cli_ctx) 3350 hy_co_client = hycos_mgmt_client_factory(cmd.cli_ctx, cmd.cli_ctx) 3351 namespace_client = namespaces_mgmt_client_factory(cmd.cli_ctx, cmd.cli_ctx) 3352 3353 hy_co_id = '' 3354 for n in namespace_client.list(): 3355 logger.warning(n.name) 3356 if n.name == namespace: 3357 hy_co_id = n.id 3358 3359 if hy_co_id == '': 3360 raise ResourceNotFoundError('Azure Service Bus Relay namespace {} was not found.'.format(namespace)) 3361 3362 i = 0 3363 hy_co_resource_group = '' 3364 hy_co_split = hy_co_id.split("/") 3365 for z in hy_co_split: 3366 if z == "resourceGroups": 3367 hy_co_resource_group = hy_co_split[i + 1] 3368 i = i + 1 3369 3370 # calling the relay API to get information about the hybrid connection 3371 hy_co = hy_co_client.get(hy_co_resource_group, namespace, hybrid_connection) 3372 3373 # if the hybrid connection does not have a default sender authorization 3374 # rule, create it 3375 hy_co_rules = hy_co_client.list_authorization_rules(hy_co_resource_group, namespace, hybrid_connection) 3376 has_default_sender_key = False 3377 for r in hy_co_rules: 3378 if r.name.lower() == "defaultsender": 3379 for z in r.rights: 3380 if z == z.send: 3381 has_default_sender_key = True 3382 3383 if not has_default_sender_key: 3384 rights = [AccessRights.send] 3385 hy_co_client.create_or_update_authorization_rule(hy_co_resource_group, namespace, hybrid_connection, 3386 "defaultSender", rights) 3387 3388 hy_co_keys = hy_co_client.list_keys(hy_co_resource_group, namespace, hybrid_connection, "defaultSender") 3389 hy_co_info = hy_co.id 3390 hy_co_metadata = ast.literal_eval(hy_co.user_metadata) 3391 hy_co_hostname = '' 3392 for x in hy_co_metadata: 3393 if x["key"] == "endpoint": 3394 hy_co_hostname = x["value"] 3395 3396 hostname_parts = hy_co_hostname.split(":") 3397 hostname = hostname_parts[0] 3398 port = hostname_parts[1] 3399 id_parameters = hy_co_info.split("/") 3400 3401 # populate object with information from the hybrid connection, and set it 3402 # on webapp 3403 3404 hc = HybridConnection(service_bus_namespace=id_parameters[8], 3405 relay_name=hybrid_connection, 3406 relay_arm_uri=hy_co_info, 3407 hostname=hostname, 3408 port=port, 3409 send_key_name="defaultSender", 3410 send_key_value=hy_co_keys.primary_key, 3411 service_bus_suffix=".servicebus.windows.net") 3412 3413 if slot is None: 3414 return_hc = web_client.web_apps.create_or_update_hybrid_connection(resource_group_name, name, namespace, 3415 hybrid_connection, hc) 3416 else: 3417 return_hc = web_client.web_apps.create_or_update_hybrid_connection_slot(resource_group_name, name, namespace, 3418 hybrid_connection, slot, hc) 3419 3420 # reformats hybrid connection, to prune unnecessary fields 3421 resourceGroup = return_hc.id.split("/") 3422 mod_hc = { 3423 "hostname": return_hc.hostname, 3424 "id": return_hc.id, 3425 "location": return_hc.additional_properties["location"], 3426 "name": return_hc.name, 3427 "port": return_hc.port, 3428 "relayArmUri": return_hc.relay_arm_uri, 3429 "resourceGroup": resourceGroup[4], 3430 "serviceBusNamespace": return_hc.service_bus_namespace, 3431 "serviceBusSuffix": return_hc.service_bus_suffix 3432 } 3433 return mod_hc 3434 3435 3436# set the key the apps use to connect with the hybrid connection 3437def set_hc_key(cmd, plan, resource_group_name, namespace, hybrid_connection, key_type): 3438 HybridConnection = cmd.get_models('HybridConnection') 3439 web_client = web_client_factory(cmd.cli_ctx) 3440 3441 # extract the hybrid connection resource group 3442 asp_hy_co = web_client.app_service_plans.get_hybrid_connection(resource_group_name, plan, 3443 namespace, hybrid_connection) 3444 arm_uri = asp_hy_co.relay_arm_uri 3445 split_uri = arm_uri.split("resourceGroups/") 3446 resource_group_strings = split_uri[1].split('/') 3447 relay_resource_group = resource_group_strings[0] 3448 3449 hy_co_client = hycos_mgmt_client_factory(cmd.cli_ctx, cmd.cli_ctx) 3450 # calling the relay function to obtain information about the hc in question 3451 hy_co = hy_co_client.get(relay_resource_group, namespace, hybrid_connection) 3452 3453 # if the hybrid connection does not have a default sender authorization 3454 # rule, create it 3455 hy_co_rules = hy_co_client.list_authorization_rules(relay_resource_group, namespace, hybrid_connection) 3456 has_default_sender_key = False 3457 for r in hy_co_rules: 3458 if r.name.lower() == "defaultsender": 3459 for z in r.rights: 3460 if z == z.send: 3461 has_default_sender_key = True 3462 3463 if not has_default_sender_key: 3464 rights = [AccessRights.send] 3465 hy_co_client.create_or_update_authorization_rule(relay_resource_group, namespace, hybrid_connection, 3466 "defaultSender", rights) 3467 3468 hy_co_keys = hy_co_client.list_keys(relay_resource_group, namespace, hybrid_connection, "defaultSender") 3469 hy_co_metadata = ast.literal_eval(hy_co.user_metadata) 3470 hy_co_hostname = 0 3471 for x in hy_co_metadata: 3472 if x["key"] == "endpoint": 3473 hy_co_hostname = x["value"] 3474 3475 hostname_parts = hy_co_hostname.split(":") 3476 hostname = hostname_parts[0] 3477 port = hostname_parts[1] 3478 3479 key = "empty" 3480 if key_type.lower() == "primary": 3481 key = hy_co_keys.primary_key 3482 elif key_type.lower() == "secondary": 3483 key = hy_co_keys.secondary_key 3484 # enures input is correct 3485 if key == "empty": 3486 logger.warning("Key type is invalid - must be primary or secondary") 3487 return 3488 3489 apps = web_client.app_service_plans.list_web_apps_by_hybrid_connection(resource_group_name, plan, namespace, 3490 hybrid_connection) 3491 # changes the key for every app that uses that hybrid connection 3492 for x in apps: 3493 app_info = ast.literal_eval(x) 3494 app_name = app_info["name"] 3495 app_id = app_info["id"] 3496 id_split = app_id.split("/") 3497 app_resource_group = id_split[4] 3498 hc = HybridConnection(service_bus_namespace=namespace, relay_name=hybrid_connection, 3499 relay_arm_uri=arm_uri, hostname=hostname, port=port, send_key_name="defaultSender", 3500 send_key_value=key) 3501 web_client.web_apps.update_hybrid_connection(app_resource_group, app_name, namespace, 3502 hybrid_connection, hc) 3503 3504 return web_client.app_service_plans.list_web_apps_by_hybrid_connection(resource_group_name, plan, 3505 namespace, hybrid_connection) 3506 3507 3508def appservice_list_vnet(cmd, resource_group_name, plan): 3509 web_client = web_client_factory(cmd.cli_ctx) 3510 return web_client.app_service_plans.list_vnets(resource_group_name, plan) 3511 3512 3513def remove_hc(cmd, resource_group_name, name, namespace, hybrid_connection, slot=None): 3514 linux_webapp = show_webapp(cmd, resource_group_name, name, slot) 3515 is_linux = linux_webapp.reserved 3516 if is_linux: 3517 return logger.warning("hybrid connections not supported on a linux app.") 3518 3519 client = web_client_factory(cmd.cli_ctx) 3520 if slot is None: 3521 return_hc = client.web_apps.delete_hybrid_connection(resource_group_name, name, namespace, hybrid_connection) 3522 else: 3523 return_hc = client.web_apps.delete_hybrid_connection_slot(resource_group_name, name, namespace, 3524 hybrid_connection, slot) 3525 return return_hc 3526 3527 3528def list_vnet_integration(cmd, name, resource_group_name, slot=None): 3529 client = web_client_factory(cmd.cli_ctx) 3530 if slot is None: 3531 result = list(client.web_apps.list_vnet_connections(resource_group_name, name)) 3532 else: 3533 result = list(client.web_apps.list_vnet_connections_slot(resource_group_name, name, slot)) 3534 mod_list = [] 3535 3536 # reformats the vnet entry, removing unecessary information 3537 for x in result: 3538 # removes GUIDs from name and id 3539 longName = x.name 3540 if '_' in longName: 3541 usIndex = longName.index('_') 3542 shortName = longName[usIndex + 1:] 3543 else: 3544 shortName = longName 3545 v_id = x.id 3546 lastSlash = v_id.rindex('/') 3547 shortId = v_id[:lastSlash] + '/' + shortName 3548 # extracts desired fields 3549 certThumbprint = x.cert_thumbprint 3550 location = x.additional_properties["location"] 3551 v_type = x.type 3552 vnet_resource_id = x.vnet_resource_id 3553 id_strings = v_id.split('/') 3554 resourceGroup = id_strings[4] 3555 routes = x.routes 3556 3557 vnet_mod = {"certThumbprint": certThumbprint, 3558 "id": shortId, 3559 "location": location, 3560 "name": shortName, 3561 "resourceGroup": resourceGroup, 3562 "routes": routes, 3563 "type": v_type, 3564 "vnetResourceId": vnet_resource_id} 3565 mod_list.append(vnet_mod) 3566 3567 return mod_list 3568 3569 3570def add_vnet_integration(cmd, name, resource_group_name, vnet, subnet, slot=None, skip_delegation_check=False): 3571 SwiftVirtualNetwork = cmd.get_models('SwiftVirtualNetwork') 3572 Delegation = cmd.get_models('Delegation', resource_type=ResourceType.MGMT_NETWORK) 3573 client = web_client_factory(cmd.cli_ctx) 3574 vnet_client = network_client_factory(cmd.cli_ctx) 3575 3576 subnet_resource_id = _validate_subnet(cmd.cli_ctx, subnet, vnet, resource_group_name) 3577 3578 if slot is None: 3579 swift_connection_info = client.web_apps.get_swift_virtual_network_connection(resource_group_name, name) 3580 else: 3581 swift_connection_info = client.web_apps.get_swift_virtual_network_connection_slot(resource_group_name, 3582 name, slot) 3583 # check to see if the connection would be supported 3584 if swift_connection_info.swift_supported is not True: 3585 return logger.warning("""Your app must be in an Azure App Service deployment that is 3586 capable of scaling up to Premium v2\nLearn more: 3587 https://go.microsoft.com/fwlink/?linkid=2060115&clcid=0x409""") 3588 3589 subnet_id_parts = parse_resource_id(subnet_resource_id) 3590 subnet_subscription_id = subnet_id_parts['subscription'] 3591 vnet_name = subnet_id_parts['name'] 3592 vnet_resource_group = subnet_id_parts['resource_group'] 3593 subnet_name = subnet_id_parts['child_name_1'] 3594 3595 if skip_delegation_check: 3596 logger.warning('Skipping delegation check. Ensure that subnet is delegated to Microsoft.Web/serverFarms.' 3597 ' Missing delegation can cause "Bad Request" error.') 3598 else: 3599 from azure.cli.core.commands.client_factory import get_subscription_id 3600 if get_subscription_id(cmd.cli_ctx).lower() != subnet_subscription_id.lower(): 3601 logger.warning('Cannot validate subnet in other subscription for delegation to Microsoft.Web/serverFarms.' 3602 ' Missing delegation can cause "Bad Request" error.') 3603 else: 3604 subnetObj = vnet_client.subnets.get(vnet_resource_group, vnet_name, subnet_name) 3605 delegations = subnetObj.delegations 3606 delegated = False 3607 for d in delegations: 3608 if d.service_name.lower() == "microsoft.web/serverfarms".lower(): 3609 delegated = True 3610 3611 if not delegated: 3612 subnetObj.delegations = [Delegation(name="delegation", service_name="Microsoft.Web/serverFarms")] 3613 vnet_client.subnets.begin_create_or_update(vnet_resource_group, vnet_name, subnet_name, 3614 subnet_parameters=subnetObj) 3615 3616 swiftVnet = SwiftVirtualNetwork(subnet_resource_id=subnet_resource_id, 3617 swift_supported=True) 3618 return_vnet = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 3619 'create_or_update_swift_virtual_network_connection', slot, swiftVnet) 3620 3621 # Enalbe Route All configuration 3622 config = get_site_configs(cmd, resource_group_name, name, slot) 3623 if config.vnet_route_all_enabled is not True: 3624 config = update_site_configs(cmd, resource_group_name, name, slot=slot, vnet_route_all_enabled='true') 3625 3626 # reformats the vnet entry, removing unnecessary information 3627 id_strings = return_vnet.id.split('/') 3628 resourceGroup = id_strings[4] 3629 mod_vnet = { 3630 "id": return_vnet.id, 3631 "location": return_vnet.additional_properties["location"], 3632 "name": return_vnet.name, 3633 "resourceGroup": resourceGroup, 3634 "subnetResourceId": return_vnet.subnet_resource_id 3635 } 3636 3637 return mod_vnet 3638 3639 3640def _validate_subnet(cli_ctx, subnet, vnet, resource_group_name): 3641 subnet_is_id = is_valid_resource_id(subnet) 3642 if subnet_is_id: 3643 subnet_id_parts = parse_resource_id(subnet) 3644 vnet_name = subnet_id_parts['name'] 3645 if not (vnet_name.lower() == vnet.lower() or subnet.startswith(vnet)): 3646 logger.warning('Subnet ID is valid. Ignoring vNet input.') 3647 return subnet 3648 3649 vnet_is_id = is_valid_resource_id(vnet) 3650 if vnet_is_id: 3651 vnet_id_parts = parse_resource_id(vnet) 3652 return resource_id( 3653 subscription=vnet_id_parts['subscription'], 3654 resource_group=vnet_id_parts['resource_group'], 3655 namespace='Microsoft.Network', 3656 type='virtualNetworks', 3657 name=vnet_id_parts['name'], 3658 child_type_1='subnets', 3659 child_name_1=subnet) 3660 3661 # Reuse logic from existing command to stay backwards compatible 3662 vnet_client = network_client_factory(cli_ctx) 3663 list_all_vnets = vnet_client.virtual_networks.list_all() 3664 3665 vnets = [] 3666 for v in list_all_vnets: 3667 if vnet in (v.name, v.id): 3668 vnet_details = parse_resource_id(v.id) 3669 vnet_resource_group = vnet_details['resource_group'] 3670 vnets.append((v.id, v.name, vnet_resource_group)) 3671 3672 if not vnets: 3673 return logger.warning("The virtual network %s was not found in the subscription.", vnet) 3674 3675 # If more than one vnet, try to use one from same resource group. Otherwise, use first and log the vnet resource id 3676 found_vnet = [v for v in vnets if v[2].lower() == resource_group_name.lower()] 3677 if not found_vnet: 3678 found_vnet = [vnets[0]] 3679 3680 (vnet_id, vnet, vnet_resource_group) = found_vnet[0] 3681 if len(vnets) > 1: 3682 logger.warning("Multiple virtual networks of name %s were found. Using virtual network with resource ID: %s. " 3683 "To use a different virtual network, specify the virtual network resource ID using --vnet.", 3684 vnet, vnet_id) 3685 vnet_id_parts = parse_resource_id(vnet_id) 3686 return resource_id( 3687 subscription=vnet_id_parts['subscription'], 3688 resource_group=vnet_id_parts['resource_group'], 3689 namespace='Microsoft.Network', 3690 type='virtualNetworks', 3691 name=vnet_id_parts['name'], 3692 child_type_1='subnets', 3693 child_name_1=subnet) 3694 3695 3696def remove_vnet_integration(cmd, name, resource_group_name, slot=None): 3697 client = web_client_factory(cmd.cli_ctx) 3698 if slot is None: 3699 return_vnet = client.web_apps.delete_swift_virtual_network(resource_group_name, name) 3700 else: 3701 return_vnet = client.web_apps.delete_swift_virtual_network_slot(resource_group_name, name, slot) 3702 return return_vnet 3703 3704 3705def get_history_triggered_webjob(cmd, resource_group_name, name, webjob_name, slot=None): 3706 client = web_client_factory(cmd.cli_ctx) 3707 if slot: 3708 return client.web_apps.list_triggered_web_job_history_slot(resource_group_name, name, webjob_name, slot) 3709 return client.web_apps.list_triggered_web_job_history(resource_group_name, name, webjob_name) 3710 3711 3712def webapp_up(cmd, name=None, resource_group_name=None, plan=None, location=None, sku=None, # pylint: disable=too-many-statements,too-many-branches 3713 os_type=None, runtime=None, dryrun=False, logs=False, launch_browser=False, html=False, 3714 app_service_environment=None): 3715 if not name: 3716 name = generate_default_app_name(cmd) 3717 3718 import os 3719 AppServicePlan = cmd.get_models('AppServicePlan') 3720 src_dir = os.getcwd() 3721 _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) 3722 client = web_client_factory(cmd.cli_ctx) 3723 user = get_profile_username() 3724 _create_new_rg = False 3725 _site_availability = get_site_availability(cmd, name) 3726 _create_new_app = _site_availability.name_available 3727 os_name = os_type if os_type else detect_os_form_src(src_dir, html) 3728 _is_linux = os_name.lower() == 'linux' 3729 3730 if runtime and html: 3731 raise CLIError('Conflicting parameters: cannot have both --runtime and --html specified.') 3732 3733 if runtime: 3734 helper = _StackRuntimeHelper(cmd, client, linux=_is_linux) 3735 runtime = helper.remove_delimiters(runtime) 3736 match = helper.resolve(runtime) 3737 if not match: 3738 if _is_linux: 3739 raise CLIError("Linux runtime '{}' is not supported." 3740 " Please invoke 'az webapp list-runtimes --linux' to cross check".format(runtime)) 3741 raise CLIError("Windows runtime '{}' is not supported." 3742 " Please invoke 'az webapp list-runtimes' to cross check".format(runtime)) 3743 3744 language = runtime.split('|')[0] 3745 version_used_create = '|'.join(runtime.split('|')[1:]) 3746 detected_version = '-' 3747 else: 3748 # detect the version 3749 _lang_details = get_lang_from_content(src_dir, html) 3750 language = _lang_details.get('language') 3751 _data = get_runtime_version_details(_lang_details.get('file_loc'), language) 3752 version_used_create = _data.get('to_create') 3753 detected_version = _data.get('detected') 3754 3755 runtime_version = "{}|{}".format(language, version_used_create) if \ 3756 version_used_create != "-" else version_used_create 3757 site_config = None 3758 3759 if not _create_new_app: # App exists, or App name unavailable 3760 if _site_availability.reason == 'Invalid': 3761 raise CLIError(_site_availability.message) 3762 # Get the ASP & RG info, if the ASP & RG parameters are provided we use those else we need to find those 3763 logger.warning("Webapp '%s' already exists. The command will deploy contents to the existing app.", name) 3764 app_details = get_app_details(cmd, name) 3765 if app_details is None: 3766 raise CLIError("Unable to retrieve details of the existing app '{}'. Please check that the app " 3767 "is a part of the current subscription if updating an existing app. If creating " 3768 "a new app, app names must be globally unique. Please try a more unique name or " 3769 "leave unspecified to receive a randomly generated name.".format(name)) 3770 current_rg = app_details.resource_group 3771 if resource_group_name is not None and (resource_group_name.lower() != current_rg.lower()): 3772 raise CLIError("The webapp '{}' exists in ResourceGroup '{}' and does not " 3773 "match the value entered '{}'. Please re-run command with the " 3774 "correct parameters.". format(name, current_rg, resource_group_name)) 3775 rg_name = resource_group_name or current_rg 3776 if location is None: 3777 loc = app_details.location.replace(" ", "").lower() 3778 else: 3779 loc = location.replace(" ", "").lower() 3780 plan_details = parse_resource_id(app_details.server_farm_id) 3781 current_plan = plan_details['name'] 3782 if plan is not None and current_plan.lower() != plan.lower(): 3783 raise CLIError("The plan name entered '{}' does not match the plan name that the webapp is hosted in '{}'." 3784 "Please check if you have configured defaults for plan name and re-run command." 3785 .format(plan, current_plan)) 3786 plan = plan or plan_details['name'] 3787 plan_info = client.app_service_plans.get(plan_details['resource_group'], plan) 3788 sku = plan_info.sku.name if isinstance(plan_info, AppServicePlan) else 'Free' 3789 current_os = 'Linux' if plan_info.reserved else 'Windows' 3790 # Raise error if current OS of the app is different from the current one 3791 if current_os.lower() != os_name.lower(): 3792 raise CLIError("The webapp '{}' is a {} app. The code detected at '{}' will default to " 3793 "'{}'. Please create a new app " 3794 "to continue this operation. For more information on default behaviors, " 3795 "see https://docs.microsoft.com/cli/azure/webapp?view=azure-cli-latest#az_webapp_up." 3796 .format(name, current_os, src_dir, os_name)) 3797 _is_linux = plan_info.reserved 3798 # for an existing app check if the runtime version needs to be updated 3799 # Get site config to check the runtime version 3800 site_config = client.web_apps.get_configuration(rg_name, name) 3801 else: # need to create new app, check if we need to use default RG or use user entered values 3802 logger.warning("The webapp '%s' doesn't exist", name) 3803 sku = get_sku_to_use(src_dir, html, sku, runtime) 3804 loc = set_location(cmd, sku, location) 3805 rg_name = get_rg_to_use(user, loc, os_name, resource_group_name) 3806 _create_new_rg = not check_resource_group_exists(cmd, rg_name) 3807 plan = get_plan_to_use(cmd=cmd, 3808 user=user, 3809 os_name=os_name, 3810 loc=loc, 3811 sku=sku, 3812 create_rg=_create_new_rg, 3813 resource_group_name=rg_name, 3814 plan=plan) 3815 dry_run_str = r""" { 3816 "name" : "%s", 3817 "appserviceplan" : "%s", 3818 "resourcegroup" : "%s", 3819 "sku": "%s", 3820 "os": "%s", 3821 "location" : "%s", 3822 "src_path" : "%s", 3823 "runtime_version_detected": "%s", 3824 "runtime_version": "%s" 3825 } 3826 """ % (name, plan, rg_name, get_sku_name(sku), os_name, loc, _src_path_escaped, detected_version, 3827 runtime_version) 3828 create_json = json.loads(dry_run_str) 3829 3830 if dryrun: 3831 logger.warning("Web app will be created with the below configuration,re-run command " 3832 "without the --dryrun flag to create & deploy a new app") 3833 return create_json 3834 3835 if _create_new_rg: 3836 logger.warning("Creating Resource group '%s' ...", rg_name) 3837 create_resource_group(cmd, rg_name, loc) 3838 logger.warning("Resource group creation complete") 3839 # create ASP 3840 logger.warning("Creating AppServicePlan '%s' ...", plan) 3841 # we will always call the ASP create or update API so that in case of re-deployment, if the SKU or plan setting are 3842 # updated we update those 3843 try: 3844 create_app_service_plan(cmd, rg_name, plan, _is_linux, hyper_v=False, per_site_scaling=False, sku=sku, 3845 number_of_workers=1 if _is_linux else None, location=loc, 3846 app_service_environment=app_service_environment) 3847 except Exception as ex: # pylint: disable=broad-except 3848 if ex.response.status_code == 409: # catch 409 conflict when trying to create existing ASP in diff location 3849 try: 3850 response_content = json.loads(ex.response._content.decode('utf-8')) # pylint: disable=protected-access 3851 except Exception: # pylint: disable=broad-except 3852 raise CLIInternalError(ex) 3853 raise UnclassifiedUserFault(response_content['error']['message']) 3854 raise AzureResponseError(ex) 3855 3856 if _create_new_app: 3857 logger.warning("Creating webapp '%s' ...", name) 3858 create_webapp(cmd, rg_name, name, plan, runtime_version if not html else None, 3859 using_webapp_up=True, language=language) 3860 _configure_default_logging(cmd, rg_name, name) 3861 else: # for existing app if we might need to update the stack runtime settings 3862 helper = _StackRuntimeHelper(cmd, client, linux=_is_linux) 3863 match = helper.resolve(runtime_version) 3864 3865 if os_name.lower() == 'linux' and site_config.linux_fx_version != runtime_version: 3866 if match and site_config.linux_fx_version != match['configs']['linux_fx_version']: 3867 logger.warning('Updating runtime version from %s to %s', 3868 site_config.linux_fx_version, match['configs']['linux_fx_version']) 3869 update_site_configs(cmd, rg_name, name, linux_fx_version=match['configs']['linux_fx_version']) 3870 logger.warning('Waiting for runtime version to propagate ...') 3871 time.sleep(30) # wait for kudu to get updated runtime before zipdeploy. No way to poll for this 3872 elif not match: 3873 logger.warning('Updating runtime version from %s to %s', 3874 site_config.linux_fx_version, runtime_version) 3875 update_site_configs(cmd, rg_name, name, linux_fx_version=runtime_version) 3876 logger.warning('Waiting for runtime version to propagate ...') 3877 time.sleep(30) # wait for kudu to get updated runtime before zipdeploy. No way to poll for this 3878 elif os_name.lower() == 'windows': 3879 # may need to update stack runtime settings. For node its site_config.app_settings, otherwise site_config 3880 if match: 3881 _update_app_settings_for_windows_if_needed(cmd, rg_name, name, match, site_config, runtime_version) 3882 create_json['runtime_version'] = runtime_version 3883 # Zip contents & Deploy 3884 logger.warning("Creating zip with contents of dir %s ...", src_dir) 3885 # zip contents & deploy 3886 zip_file_path = zip_contents_from_dir(src_dir, language) 3887 enable_zip_deploy(cmd, rg_name, name, zip_file_path) 3888 3889 if launch_browser: 3890 logger.warning("Launching app using default browser") 3891 view_in_browser(cmd, rg_name, name, None, logs) 3892 else: 3893 _url = _get_url(cmd, rg_name, name) 3894 logger.warning("You can launch the app at %s", _url) 3895 create_json.update({'URL': _url}) 3896 3897 if logs: 3898 _configure_default_logging(cmd, rg_name, name) 3899 return get_streaming_log(cmd, rg_name, name) 3900 with ConfiguredDefaultSetter(cmd.cli_ctx.config, True): 3901 cmd.cli_ctx.config.set_value('defaults', 'group', rg_name) 3902 cmd.cli_ctx.config.set_value('defaults', 'sku', sku) 3903 cmd.cli_ctx.config.set_value('defaults', 'appserviceplan', plan) 3904 cmd.cli_ctx.config.set_value('defaults', 'location', loc) 3905 cmd.cli_ctx.config.set_value('defaults', 'web', name) 3906 return create_json 3907 3908 3909def _update_app_settings_for_windows_if_needed(cmd, rg_name, name, match, site_config, runtime_version): 3910 update_needed = False 3911 if 'node' in runtime_version: 3912 settings = [] 3913 for k, v in match['configs'].items(): 3914 for app_setting in site_config.app_settings: 3915 if app_setting.name == k and app_setting.value != v: 3916 update_needed = True 3917 settings.append('%s=%s', k, v) 3918 if update_needed: 3919 logger.warning('Updating runtime version to %s', runtime_version) 3920 update_app_settings(cmd, rg_name, name, settings=settings, slot=None, slot_settings=None) 3921 else: 3922 for k, v in match['configs'].items(): 3923 if getattr(site_config, k, None) != v: 3924 update_needed = True 3925 setattr(site_config, k, v) 3926 if update_needed: 3927 logger.warning('Updating runtime version to %s', runtime_version) 3928 update_site_configs(cmd, 3929 rg_name, 3930 name, 3931 net_framework_version=site_config.net_framework_version, 3932 php_version=site_config.php_version, 3933 python_version=site_config.python_version, 3934 java_version=site_config.java_version, 3935 java_container=site_config.java_container, 3936 java_container_version=site_config.java_container_version) 3937 3938 current_stack = get_current_stack_from_runtime(runtime_version) 3939 _update_webapp_current_stack_property_if_needed(cmd, rg_name, name, current_stack) 3940 3941 if update_needed: 3942 logger.warning('Waiting for runtime version to propagate ...') 3943 time.sleep(30) # wait for kudu to get updated runtime before zipdeploy. No way to poll for this 3944 3945 3946def _update_webapp_current_stack_property_if_needed(cmd, resource_group, name, current_stack): 3947 if not current_stack: 3948 return 3949 # portal uses this current_stack value to display correct runtime for windows webapps 3950 client = web_client_factory(cmd.cli_ctx) 3951 app_metadata = client.web_apps.list_metadata(resource_group, name) 3952 if 'CURRENT_STACK' not in app_metadata.properties or app_metadata.properties["CURRENT_STACK"] != current_stack: 3953 app_metadata.properties["CURRENT_STACK"] = current_stack 3954 client.web_apps.update_metadata(resource_group, name, metadata=app_metadata) 3955 3956 3957def _ping_scm_site(cmd, resource_group, name, instance=None): 3958 from azure.cli.core.util import should_disable_connection_verify 3959 # wake up kudu, by making an SCM call 3960 import requests 3961 # work around until the timeout limits issue for linux is investigated & fixed 3962 user_name, password = _get_site_credential(cmd.cli_ctx, resource_group, name) 3963 scm_url = _get_scm_url(cmd, resource_group, name) 3964 import urllib3 3965 authorization = urllib3.util.make_headers(basic_auth='{}:{}'.format(user_name, password)) 3966 cookies = {} 3967 if instance is not None: 3968 cookies['ARRAffinity'] = instance 3969 requests.get(scm_url + '/api/settings', headers=authorization, verify=not should_disable_connection_verify(), 3970 cookies=cookies) 3971 3972 3973def is_webapp_up(tunnel_server): 3974 return tunnel_server.is_webapp_up() 3975 3976 3977def get_tunnel(cmd, resource_group_name, name, port=None, slot=None, instance=None): 3978 webapp = show_webapp(cmd, resource_group_name, name, slot) 3979 is_linux = webapp.reserved 3980 if not is_linux: 3981 raise CLIError("Only Linux App Service Plans supported, Found a Windows App Service Plan") 3982 3983 profiles = list_publish_profiles(cmd, resource_group_name, name, slot) 3984 profile_user_name = next(p['userName'] for p in profiles) 3985 profile_user_password = next(p['userPWD'] for p in profiles) 3986 3987 if port is None: 3988 port = 0 # Will auto-select a free port from 1024-65535 3989 logger.info('No port defined, creating on random free port') 3990 3991 # Validate that we have a known instance (case-sensitive) 3992 if instance is not None: 3993 instances = list_instances(cmd, resource_group_name, name, slot=slot) 3994 instance_names = set(i.name for i in instances) 3995 if instance not in instance_names: 3996 if slot is not None: 3997 raise CLIError("The provided instance '{}' is not valid for this webapp and slot.".format(instance)) 3998 raise CLIError("The provided instance '{}' is not valid for this webapp.".format(instance)) 3999 4000 scm_url = _get_scm_url(cmd, resource_group_name, name, slot) 4001 4002 tunnel_server = TunnelServer('', port, scm_url, profile_user_name, profile_user_password, instance) 4003 _ping_scm_site(cmd, resource_group_name, name, instance=instance) 4004 4005 _wait_for_webapp(tunnel_server) 4006 return tunnel_server 4007 4008 4009def create_tunnel(cmd, resource_group_name, name, port=None, slot=None, timeout=None, instance=None): 4010 tunnel_server = get_tunnel(cmd, resource_group_name, name, port, slot, instance) 4011 4012 t = threading.Thread(target=_start_tunnel, args=(tunnel_server,)) 4013 t.daemon = True 4014 t.start() 4015 4016 logger.warning('Opening tunnel on port: %s', tunnel_server.local_port) 4017 4018 config = get_site_configs(cmd, resource_group_name, name, slot) 4019 if config.remote_debugging_enabled: 4020 logger.warning('Tunnel is ready, connect on port %s', tunnel_server.local_port) 4021 else: 4022 ssh_user_name = 'root' 4023 ssh_user_password = 'Docker!' 4024 logger.warning('SSH is available { username: %s, password: %s }', ssh_user_name, ssh_user_password) 4025 4026 logger.warning('Ctrl + C to close') 4027 4028 if timeout: 4029 time.sleep(int(timeout)) 4030 else: 4031 while t.is_alive(): 4032 time.sleep(5) 4033 4034 4035def create_tunnel_and_session(cmd, resource_group_name, name, port=None, slot=None, timeout=None, instance=None): 4036 tunnel_server = get_tunnel(cmd, resource_group_name, name, port, slot, instance) 4037 4038 t = threading.Thread(target=_start_tunnel, args=(tunnel_server,)) 4039 t.daemon = True 4040 t.start() 4041 4042 ssh_user_name = 'root' 4043 ssh_user_password = 'Docker!' 4044 4045 s = threading.Thread(target=_start_ssh_session, 4046 args=('localhost', tunnel_server.get_port(), ssh_user_name, ssh_user_password)) 4047 s.daemon = True 4048 s.start() 4049 4050 if timeout: 4051 time.sleep(int(timeout)) 4052 else: 4053 while s.is_alive() and t.is_alive(): 4054 time.sleep(5) 4055 4056 4057def perform_onedeploy(cmd, 4058 resource_group_name, 4059 name, 4060 src_path=None, 4061 src_url=None, 4062 target_path=None, 4063 artifact_type=None, 4064 is_async=None, 4065 restart=None, 4066 clean=None, 4067 ignore_stack=None, 4068 timeout=None, 4069 slot=None): 4070 params = OneDeployParams() 4071 4072 params.cmd = cmd 4073 params.resource_group_name = resource_group_name 4074 params.webapp_name = name 4075 params.src_path = src_path 4076 params.src_url = src_url 4077 params.target_path = target_path 4078 params.artifact_type = artifact_type 4079 params.is_async_deployment = is_async 4080 params.should_restart = restart 4081 params.is_clean_deployment = clean 4082 params.should_ignore_stack = ignore_stack 4083 params.timeout = timeout 4084 params.slot = slot 4085 4086 return _perform_onedeploy_internal(params) 4087 4088 4089# Class for OneDeploy parameters 4090# pylint: disable=too-many-instance-attributes,too-few-public-methods 4091class OneDeployParams: 4092 def __init__(self): 4093 self.cmd = None 4094 self.resource_group_name = None 4095 self.webapp_name = None 4096 self.src_path = None 4097 self.src_url = None 4098 self.artifact_type = None 4099 self.is_async_deployment = None 4100 self.target_path = None 4101 self.should_restart = None 4102 self.is_clean_deployment = None 4103 self.should_ignore_stack = None 4104 self.timeout = None 4105 self.slot = None 4106# pylint: enable=too-many-instance-attributes,too-few-public-methods 4107 4108 4109def _build_onedeploy_url(params): 4110 scm_url = _get_scm_url(params.cmd, params.resource_group_name, params.webapp_name, params.slot) 4111 deploy_url = scm_url + '/api/publish?type=' + params.artifact_type 4112 4113 if params.is_async_deployment is not None: 4114 deploy_url = deploy_url + '&async=' + str(params.is_async_deployment) 4115 4116 if params.should_restart is not None: 4117 deploy_url = deploy_url + '&restart=' + str(params.should_restart) 4118 4119 if params.is_clean_deployment is not None: 4120 deploy_url = deploy_url + '&clean=' + str(params.is_clean_deployment) 4121 4122 if params.should_ignore_stack is not None: 4123 deploy_url = deploy_url + '&ignorestack=' + str(params.should_ignore_stack) 4124 4125 if params.target_path is not None: 4126 deploy_url = deploy_url + '&path=' + params.target_path 4127 4128 return deploy_url 4129 4130 4131def _get_onedeploy_status_url(params): 4132 scm_url = _get_scm_url(params.cmd, params.resource_group_name, params.webapp_name, params.slot) 4133 return scm_url + '/api/deployments/latest' 4134 4135 4136def _get_basic_headers(params): 4137 import urllib3 4138 4139 user_name, password = _get_site_credential(params.cmd.cli_ctx, params.resource_group_name, 4140 params.webapp_name, params.slot) 4141 4142 if params.src_path: 4143 content_type = 'application/octet-stream' 4144 elif params.src_url: 4145 content_type = 'application/json' 4146 else: 4147 raise CLIError('Unable to determine source location of the artifact being deployed') 4148 4149 headers = urllib3.util.make_headers(basic_auth='{0}:{1}'.format(user_name, password)) 4150 headers['Cache-Control'] = 'no-cache' 4151 headers['User-Agent'] = get_az_user_agent() 4152 headers['Content-Type'] = content_type 4153 4154 return headers 4155 4156 4157def _get_onedeploy_request_body(params): 4158 import os 4159 4160 if params.src_path: 4161 logger.info('Deploying from local path: %s', params.src_path) 4162 try: 4163 with open(os.path.realpath(os.path.expanduser(params.src_path)), 'rb') as fs: 4164 body = fs.read() 4165 except Exception as e: # pylint: disable=broad-except 4166 raise CLIError("Either '{}' is not a valid local file path or you do not have permissions to access it" 4167 .format(params.src_path)) from e 4168 elif params.src_url: 4169 logger.info('Deploying from URL: %s', params.src_url) 4170 body = json.dumps({ 4171 "packageUri": params.src_url 4172 }) 4173 else: 4174 raise CLIError('Unable to determine source location of the artifact being deployed') 4175 4176 return body 4177 4178 4179def _update_artifact_type(params): 4180 import ntpath 4181 4182 if params.artifact_type is not None: 4183 return 4184 4185 # Interpret deployment type from the file extension if the type parameter is not passed 4186 file_name = ntpath.basename(params.src_path) 4187 file_extension = file_name.split(".", 1)[1] 4188 if file_extension in ('war', 'jar', 'ear', 'zip'): 4189 params.artifact_type = file_extension 4190 elif file_extension in ('sh', 'bat'): 4191 params.artifact_type = 'startup' 4192 else: 4193 params.artifact_type = 'static' 4194 logger.warning("Deployment type: %s. To override deloyment type, please specify the --type parameter. " 4195 "Possible values: war, jar, ear, zip, startup, script, static", params.artifact_type) 4196 4197 4198def _make_onedeploy_request(params): 4199 import requests 4200 4201 from azure.cli.core.util import ( 4202 should_disable_connection_verify, 4203 ) 4204 4205 # Build the request body, headers, API URL and status URL 4206 body = _get_onedeploy_request_body(params) 4207 headers = _get_basic_headers(params) 4208 deploy_url = _build_onedeploy_url(params) 4209 deployment_status_url = _get_onedeploy_status_url(params) 4210 4211 logger.info("Deployment API: %s", deploy_url) 4212 response = requests.post(deploy_url, data=body, headers=headers, verify=not should_disable_connection_verify()) 4213 4214 # For debugging purposes only, you can change the async deployment into a sync deployment by polling the API status 4215 # For that, set poll_async_deployment_for_debugging=True 4216 poll_async_deployment_for_debugging = True 4217 4218 # check the status of async deployment 4219 if response.status_code == 202 or response.status_code == 200: 4220 response_body = None 4221 if poll_async_deployment_for_debugging: 4222 logger.info('Polling the status of async deployment') 4223 response_body = _check_zip_deployment_status(params.cmd, params.resource_group_name, params.webapp_name, 4224 deployment_status_url, headers, params.timeout) 4225 logger.info('Async deployment complete. Server response: %s', response_body) 4226 return response_body 4227 4228 # API not available yet! 4229 if response.status_code == 404: 4230 raise CLIError("This API isn't available in this environment yet!") 4231 4232 # check if there's an ongoing process 4233 if response.status_code == 409: 4234 raise CLIError("Another deployment is in progress. Please wait until that process is complete before " 4235 "starting a new deployment. You can track the ongoing deployment at {}" 4236 .format(deployment_status_url)) 4237 4238 # check if an error occured during deployment 4239 if response.status_code: 4240 raise CLIError("An error occured during deployment. Status Code: {}, Details: {}" 4241 .format(response.status_code, response.text)) 4242 4243 4244# OneDeploy 4245def _perform_onedeploy_internal(params): 4246 4247 # Update artifact type, if required 4248 _update_artifact_type(params) 4249 4250 # Now make the OneDeploy API call 4251 logger.info("Initiating deployment") 4252 response = _make_onedeploy_request(params) 4253 logger.info("Deployment has completed successfully") 4254 return response 4255 4256 4257def _wait_for_webapp(tunnel_server): 4258 tries = 0 4259 while True: 4260 if is_webapp_up(tunnel_server): 4261 break 4262 if tries == 0: 4263 logger.warning('Connection is not ready yet, please wait') 4264 if tries == 60: 4265 raise CLIError('SSH timeout, your app must be running before' 4266 ' it can accept SSH connections. ' 4267 'Use `az webapp log tail` to review the app startup logs.') 4268 tries = tries + 1 4269 logger.warning('.') 4270 time.sleep(1) 4271 4272 4273def _start_tunnel(tunnel_server): 4274 tunnel_server.start_server() 4275 4276 4277def _start_ssh_session(hostname, port, username, password): 4278 tries = 0 4279 while True: 4280 try: 4281 c = Connection(host=hostname, 4282 port=port, 4283 user=username, 4284 # connect_timeout=60*10, 4285 connect_kwargs={"password": password}) 4286 break 4287 except Exception as ex: # pylint: disable=broad-except 4288 logger.info(ex) 4289 if tries == 0: 4290 logger.warning('Connection is not ready yet, please wait') 4291 if tries == 60: 4292 raise CLIError("Timeout Error, Unable to establish a connection") 4293 tries = tries + 1 4294 logger.warning('.') 4295 time.sleep(1) 4296 try: 4297 c.run('cat /etc/motd', pty=True) 4298 c.run('source /etc/profile; exec $SHELL -l', pty=True) 4299 except Exception as ex: # pylint: disable=broad-except 4300 logger.info(ex) 4301 finally: 4302 c.close() 4303 4304 4305def ssh_webapp(cmd, resource_group_name, name, port=None, slot=None, timeout=None, instance=None): # pylint: disable=too-many-statements 4306 import platform 4307 if platform.system() == "Windows": 4308 webapp = show_webapp(cmd, resource_group_name, name, slot) 4309 is_linux = webapp.reserved 4310 if not is_linux: 4311 raise ValidationError("Only Linux App Service Plans supported, found a Windows App Service Plan") 4312 4313 scm_url = _get_scm_url(cmd, resource_group_name, name, slot) 4314 open_page_in_browser(scm_url + '/webssh/host') 4315 else: 4316 config = get_site_configs(cmd, resource_group_name, name, slot) 4317 if config.remote_debugging_enabled: 4318 raise ValidationError('Remote debugging is enabled, please disable') 4319 create_tunnel_and_session( 4320 cmd, resource_group_name, name, port=port, slot=slot, timeout=timeout, instance=instance) 4321 4322 4323def create_devops_pipeline( 4324 cmd, 4325 functionapp_name=None, 4326 organization_name=None, 4327 project_name=None, 4328 repository_name=None, 4329 overwrite_yaml=None, 4330 allow_force_push=None, 4331 github_pat=None, 4332 github_repository=None 4333): 4334 from .azure_devops_build_interactive import AzureDevopsBuildInteractive 4335 azure_devops_build_interactive = AzureDevopsBuildInteractive(cmd, logger, functionapp_name, 4336 organization_name, project_name, repository_name, 4337 overwrite_yaml, allow_force_push, 4338 github_pat, github_repository) 4339 return azure_devops_build_interactive.interactive_azure_devops_build() 4340 4341 4342def _configure_default_logging(cmd, rg_name, name): 4343 logger.warning("Configuring default logging for the app, if not already enabled") 4344 return config_diagnostics(cmd, rg_name, name, 4345 application_logging=True, web_server_logging='filesystem', 4346 docker_container_logging='true') 4347 4348 4349def _validate_app_service_environment_id(cli_ctx, ase, resource_group_name): 4350 ase_is_id = is_valid_resource_id(ase) 4351 if ase_is_id: 4352 return ase 4353 4354 from azure.cli.core.commands.client_factory import get_subscription_id 4355 return resource_id( 4356 subscription=get_subscription_id(cli_ctx), 4357 resource_group=resource_group_name, 4358 namespace='Microsoft.Web', 4359 type='hostingEnvironments', 4360 name=ase) 4361 4362 4363def _validate_asp_sku(app_service_environment, sku): 4364 # Isolated SKU is supported only for ASE 4365 if sku.upper() in ['I1', 'I2', 'I3', 'I1V2', 'I2V2', 'I3V2']: 4366 if not app_service_environment: 4367 raise CLIError("The pricing tier 'Isolated' is not allowed for this app service plan. Use this link to " 4368 "learn more: https://docs.microsoft.com/azure/app-service/overview-hosting-plans") 4369 else: 4370 if app_service_environment: 4371 raise CLIError("Only pricing tier 'Isolated' is allowed in this app service plan. Use this link to " 4372 "learn more: https://docs.microsoft.com/azure/app-service/overview-hosting-plans") 4373 4374 4375def _format_key_vault_id(cli_ctx, key_vault, resource_group_name): 4376 key_vault_is_id = is_valid_resource_id(key_vault) 4377 if key_vault_is_id: 4378 return key_vault 4379 4380 from azure.cli.core.commands.client_factory import get_subscription_id 4381 return resource_id( 4382 subscription=get_subscription_id(cli_ctx), 4383 resource_group=resource_group_name, 4384 namespace='Microsoft.KeyVault', 4385 type='vaults', 4386 name=key_vault) 4387 4388 4389def _verify_hostname_binding(cmd, resource_group_name, name, hostname, slot=None): 4390 hostname_bindings = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 4391 'list_host_name_bindings', slot) 4392 verified_hostname_found = False 4393 for hostname_binding in hostname_bindings: 4394 binding_name = hostname_binding.name.split('/')[-1] 4395 if binding_name.lower() == hostname and (hostname_binding.host_name_type == 'Verified' or 4396 hostname_binding.host_name_type == 'Managed'): 4397 verified_hostname_found = True 4398 4399 return verified_hostname_found 4400 4401 4402def update_host_key(cmd, resource_group_name, name, key_type, key_name, key_value=None, slot=None): 4403 # pylint: disable=protected-access 4404 key_info = KeyInfo(name=key_name, value=key_value) 4405 KeyInfo._attribute_map = { 4406 'name': {'key': 'properties.name', 'type': 'str'}, 4407 'value': {'key': 'properties.value', 'type': 'str'}, 4408 } 4409 client = web_client_factory(cmd.cli_ctx) 4410 if slot: 4411 return client.web_apps.create_or_update_host_secret_slot(resource_group_name, 4412 name, 4413 key_type, 4414 key_name, 4415 slot, key=key_info) 4416 return client.web_apps.create_or_update_host_secret(resource_group_name, 4417 name, 4418 key_type, 4419 key_name, key=key_info) 4420 4421 4422def list_host_keys(cmd, resource_group_name, name, slot=None): 4423 client = web_client_factory(cmd.cli_ctx) 4424 if slot: 4425 return client.web_apps.list_host_keys_slot(resource_group_name, name, slot) 4426 return client.web_apps.list_host_keys(resource_group_name, name) 4427 4428 4429def delete_host_key(cmd, resource_group_name, name, key_type, key_name, slot=None): 4430 client = web_client_factory(cmd.cli_ctx) 4431 if slot: 4432 return client.web_apps.delete_host_secret_slot(resource_group_name, name, key_type, key_name, slot) 4433 return client.web_apps.delete_host_secret(resource_group_name, name, key_type, key_name) 4434 4435 4436def show_function(cmd, resource_group_name, name, function_name): 4437 client = web_client_factory(cmd.cli_ctx) 4438 result = client.web_apps.get_function(resource_group_name, name, function_name) 4439 if result is None: 4440 return "Function '{}' does not exist in app '{}'".format(function_name, name) 4441 return result 4442 4443 4444def delete_function(cmd, resource_group_name, name, function_name): 4445 client = web_client_factory(cmd.cli_ctx) 4446 result = client.web_apps.delete_function(resource_group_name, name, function_name) 4447 return result 4448 4449 4450def update_function_key(cmd, resource_group_name, name, function_name, key_name, key_value=None, slot=None): 4451 # pylint: disable=protected-access 4452 key_info = KeyInfo(name=key_name, value=key_value) 4453 KeyInfo._attribute_map = { 4454 'name': {'key': 'properties.name', 'type': 'str'}, 4455 'value': {'key': 'properties.value', 'type': 'str'}, 4456 } 4457 client = web_client_factory(cmd.cli_ctx) 4458 if slot: 4459 return client.web_apps.create_or_update_function_secret_slot(resource_group_name, 4460 name, 4461 function_name, 4462 key_name, 4463 slot, 4464 key_info) 4465 return client.web_apps.create_or_update_function_secret(resource_group_name, 4466 name, 4467 function_name, 4468 key_name, 4469 key_info) 4470 4471 4472def list_function_keys(cmd, resource_group_name, name, function_name, slot=None): 4473 client = web_client_factory(cmd.cli_ctx) 4474 if slot: 4475 return client.web_apps.list_function_keys_slot(resource_group_name, name, function_name, slot) 4476 return client.web_apps.list_function_keys(resource_group_name, name, function_name) 4477 4478 4479def delete_function_key(cmd, resource_group_name, name, key_name, function_name=None, slot=None): 4480 client = web_client_factory(cmd.cli_ctx) 4481 if slot: 4482 return client.web_apps.delete_function_secret_slot(resource_group_name, name, function_name, key_name, slot) 4483 return client.web_apps.delete_function_secret(resource_group_name, name, function_name, key_name) 4484 4485 4486def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None, slot=None, # pylint: disable=too-many-statements,too-many-branches 4487 branch='master', login_with_github=False, force=False): 4488 if not token and not login_with_github: 4489 raise_missing_token_suggestion() 4490 elif not token: 4491 scopes = ["admin:repo_hook", "repo", "workflow"] 4492 token = get_github_access_token(cmd, scopes) 4493 elif token and login_with_github: 4494 logger.warning("Both token and --login-with-github flag are provided. Will use provided token") 4495 4496 # Verify resource group, app 4497 site_availability = get_site_availability(cmd, name) 4498 if site_availability.name_available or (not site_availability.name_available and 4499 site_availability.reason == 'Invalid'): 4500 raise ResourceNotFoundError( 4501 "The Resource 'Microsoft.Web/sites/%s' under resource group '%s' " 4502 "was not found." % (name, resource_group)) 4503 app_details = get_app_details(cmd, name) 4504 if app_details is None: 4505 raise ResourceNotFoundError( 4506 "Unable to retrieve details of the existing app %s. Please check that the app is a part of " 4507 "the current subscription" % name) 4508 current_rg = app_details.resource_group 4509 if resource_group is not None and (resource_group.lower() != current_rg.lower()): 4510 raise ResourceNotFoundError("The webapp %s exists in ResourceGroup %s and does not match the " 4511 "value entered %s. Please re-run command with the correct " 4512 "parameters." % (name, current_rg, resource_group)) 4513 parsed_plan_id = parse_resource_id(app_details.server_farm_id) 4514 client = web_client_factory(cmd.cli_ctx) 4515 plan_info = client.app_service_plans.get(parsed_plan_id['resource_group'], parsed_plan_id['name']) 4516 is_linux = plan_info.reserved 4517 4518 # Verify github repo 4519 from github import Github, GithubException 4520 from github.GithubException import BadCredentialsException, UnknownObjectException 4521 4522 if repo.strip()[-1] == '/': 4523 repo = repo.strip()[:-1] 4524 4525 g = Github(token) 4526 github_repo = None 4527 try: 4528 github_repo = g.get_repo(repo) 4529 try: 4530 github_repo.get_branch(branch=branch) 4531 except GithubException as e: 4532 error_msg = "Encountered GitHub error when accessing {} branch in {} repo.".format(branch, repo) 4533 if e.data and e.data['message']: 4534 error_msg += " Error: {}".format(e.data['message']) 4535 raise CLIError(error_msg) 4536 logger.warning('Verified GitHub repo and branch') 4537 except BadCredentialsException: 4538 raise CLIError("Could not authenticate to the repository. Please create a Personal Access Token and use " 4539 "the --token argument. Run 'az webapp deployment github-actions add --help' " 4540 "for more information.") 4541 except GithubException as e: 4542 error_msg = "Encountered GitHub error when accessing {} repo".format(repo) 4543 if e.data and e.data['message']: 4544 error_msg += " Error: {}".format(e.data['message']) 4545 raise CLIError(error_msg) 4546 4547 # Verify runtime 4548 app_runtime_info = _get_app_runtime_info( 4549 cmd=cmd, resource_group=resource_group, name=name, slot=slot, is_linux=is_linux) 4550 4551 app_runtime_string = None 4552 if(app_runtime_info and app_runtime_info['display_name']): 4553 app_runtime_string = app_runtime_info['display_name'] 4554 4555 github_actions_version = None 4556 if (app_runtime_info and app_runtime_info['github_actions_version']): 4557 github_actions_version = app_runtime_info['github_actions_version'] 4558 4559 if runtime and app_runtime_string: 4560 if app_runtime_string.lower() != runtime.lower(): 4561 logger.warning('The app runtime: {app_runtime_string} does not match the runtime specified: ' 4562 '{runtime}. Using the specified runtime {runtime}.') 4563 app_runtime_string = runtime 4564 elif runtime: 4565 app_runtime_string = runtime 4566 4567 if not app_runtime_string: 4568 raise CLIError('Could not detect runtime. Please specify using the --runtime flag.') 4569 4570 if not _runtime_supports_github_actions(runtime_string=app_runtime_string, is_linux=is_linux): 4571 raise CLIError("Runtime %s is not supported for GitHub Actions deployments." % app_runtime_string) 4572 4573 # Get workflow template 4574 logger.warning('Getting workflow template using runtime: %s', app_runtime_string) 4575 workflow_template = _get_workflow_template(github=g, runtime_string=app_runtime_string, is_linux=is_linux) 4576 4577 # Fill workflow template 4578 guid = str(uuid.uuid4()).replace('-', '') 4579 publish_profile_name = "AzureAppService_PublishProfile_{}".format(guid) 4580 logger.warning( 4581 'Filling workflow template with name: %s, branch: %s, version: %s, slot: %s', 4582 name, branch, github_actions_version, slot if slot else 'production') 4583 completed_workflow_file = _fill_workflow_template(content=workflow_template.decoded_content.decode(), name=name, 4584 branch=branch, slot=slot, publish_profile=publish_profile_name, 4585 version=github_actions_version) 4586 completed_workflow_file = completed_workflow_file.encode() 4587 4588 # Check if workflow exists in repo, otherwise push 4589 if slot: 4590 file_name = "{}_{}({}).yml".format(branch.replace('/', '-'), name.lower(), slot) 4591 else: 4592 file_name = "{}_{}.yml".format(branch.replace('/', '-'), name.lower()) 4593 dir_path = "{}/{}".format('.github', 'workflows') 4594 file_path = "/{}/{}".format(dir_path, file_name) 4595 try: 4596 existing_workflow_file = github_repo.get_contents(path=file_path, ref=branch) 4597 existing_publish_profile_name = _get_publish_profile_from_workflow_file( 4598 workflow_file=str(existing_workflow_file.decoded_content)) 4599 if existing_publish_profile_name: 4600 completed_workflow_file = completed_workflow_file.decode() 4601 completed_workflow_file = completed_workflow_file.replace( 4602 publish_profile_name, existing_publish_profile_name) 4603 completed_workflow_file = completed_workflow_file.encode() 4604 publish_profile_name = existing_publish_profile_name 4605 logger.warning("Existing workflow file found") 4606 if force: 4607 logger.warning("Replacing the existing workflow file") 4608 github_repo.update_file(path=file_path, message="Update workflow using Azure CLI", 4609 content=completed_workflow_file, sha=existing_workflow_file.sha, branch=branch) 4610 else: 4611 option = prompt_y_n('Replace existing workflow file?') 4612 if option: 4613 logger.warning("Replacing the existing workflow file") 4614 github_repo.update_file(path=file_path, message="Update workflow using Azure CLI", 4615 content=completed_workflow_file, sha=existing_workflow_file.sha, 4616 branch=branch) 4617 else: 4618 logger.warning("Use the existing workflow file") 4619 if existing_publish_profile_name: 4620 publish_profile_name = existing_publish_profile_name 4621 except UnknownObjectException: 4622 logger.warning("Creating new workflow file: %s", file_path) 4623 github_repo.create_file(path=file_path, message="Create workflow using Azure CLI", 4624 content=completed_workflow_file, branch=branch) 4625 4626 # Add publish profile to GitHub 4627 logger.warning('Adding publish profile to GitHub') 4628 _add_publish_profile_to_github(cmd=cmd, resource_group=resource_group, name=name, repo=repo, 4629 token=token, github_actions_secret_name=publish_profile_name, 4630 slot=slot) 4631 4632 # Set site source control properties 4633 _update_site_source_control_properties_for_gh_action( 4634 cmd=cmd, resource_group=resource_group, name=name, token=token, repo=repo, branch=branch, slot=slot) 4635 4636 github_actions_url = "https://github.com/{}/actions".format(repo) 4637 return github_actions_url 4638 4639 4640def remove_github_actions(cmd, resource_group, name, repo, token=None, slot=None, # pylint: disable=too-many-statements 4641 branch='master', login_with_github=False): 4642 if not token and not login_with_github: 4643 raise_missing_token_suggestion() 4644 elif not token: 4645 scopes = ["admin:repo_hook", "repo", "workflow"] 4646 token = get_github_access_token(cmd, scopes) 4647 elif token and login_with_github: 4648 logger.warning("Both token and --login-with-github flag are provided. Will use provided token") 4649 4650 # Verify resource group, app 4651 site_availability = get_site_availability(cmd, name) 4652 if site_availability.name_available or (not site_availability.name_available and 4653 site_availability.reason == 'Invalid'): 4654 raise CLIError("The Resource 'Microsoft.Web/sites/%s' under resource group '%s' was not found." % 4655 (name, resource_group)) 4656 app_details = get_app_details(cmd, name) 4657 if app_details is None: 4658 raise CLIError("Unable to retrieve details of the existing app %s. " 4659 "Please check that the app is a part of the current subscription" % name) 4660 current_rg = app_details.resource_group 4661 if resource_group is not None and (resource_group.lower() != current_rg.lower()): 4662 raise CLIError("The webapp %s exists in ResourceGroup %s and does not match " 4663 "the value entered %s. Please re-run command with the correct " 4664 "parameters." % (name, current_rg, resource_group)) 4665 4666 # Verify github repo 4667 from github import Github, GithubException 4668 from github.GithubException import BadCredentialsException, UnknownObjectException 4669 4670 if repo.strip()[-1] == '/': 4671 repo = repo.strip()[:-1] 4672 4673 g = Github(token) 4674 github_repo = None 4675 try: 4676 github_repo = g.get_repo(repo) 4677 try: 4678 github_repo.get_branch(branch=branch) 4679 except GithubException as e: 4680 error_msg = "Encountered GitHub error when accessing {} branch in {} repo.".format(branch, repo) 4681 if e.data and e.data['message']: 4682 error_msg += " Error: {}".format(e.data['message']) 4683 raise CLIError(error_msg) 4684 logger.warning('Verified GitHub repo and branch') 4685 except BadCredentialsException: 4686 raise CLIError("Could not authenticate to the repository. Please create a Personal Access Token and use " 4687 "the --token argument. Run 'az webapp deployment github-actions add --help' " 4688 "for more information.") 4689 except GithubException as e: 4690 error_msg = "Encountered GitHub error when accessing {} repo".format(repo) 4691 if e.data and e.data['message']: 4692 error_msg += " Error: {}".format(e.data['message']) 4693 raise CLIError(error_msg) 4694 4695 # Check if workflow exists in repo and remove 4696 file_name = "{}_{}({}).yml".format( 4697 branch.replace('/', '-'), name.lower(), slot) if slot else "{}_{}.yml".format( 4698 branch.replace('/', '-'), name.lower()) 4699 dir_path = "{}/{}".format('.github', 'workflows') 4700 file_path = "/{}/{}".format(dir_path, file_name) 4701 existing_publish_profile_name = None 4702 try: 4703 existing_workflow_file = github_repo.get_contents(path=file_path, ref=branch) 4704 existing_publish_profile_name = _get_publish_profile_from_workflow_file( 4705 workflow_file=str(existing_workflow_file.decoded_content)) 4706 logger.warning("Removing the existing workflow file") 4707 github_repo.delete_file(path=file_path, message="Removing workflow file, disconnecting github actions", 4708 sha=existing_workflow_file.sha, branch=branch) 4709 except UnknownObjectException as e: 4710 error_msg = "Error when removing workflow file." 4711 if e.data and e.data['message']: 4712 error_msg += " Error: {}".format(e.data['message']) 4713 raise CLIError(error_msg) 4714 4715 # Remove publish profile from GitHub 4716 if existing_publish_profile_name: 4717 logger.warning('Removing publish profile from GitHub') 4718 _remove_publish_profile_from_github(cmd=cmd, resource_group=resource_group, name=name, repo=repo, token=token, 4719 github_actions_secret_name=existing_publish_profile_name, slot=slot) 4720 4721 # Remove site source control properties 4722 delete_source_control(cmd=cmd, 4723 resource_group_name=resource_group, 4724 name=name, 4725 slot=slot) 4726 4727 return "Disconnected successfully." 4728 4729 4730def _get_publish_profile_from_workflow_file(workflow_file): 4731 import re 4732 publish_profile = None 4733 regex = re.search(r'publish-profile: \$\{\{ secrets\..*?\}\}', workflow_file) 4734 if regex: 4735 publish_profile = regex.group() 4736 publish_profile = publish_profile.replace('publish-profile: ${{ secrets.', '') 4737 publish_profile = publish_profile[:-2] 4738 4739 if publish_profile: 4740 return publish_profile.strip() 4741 return None 4742 4743 4744def _update_site_source_control_properties_for_gh_action(cmd, resource_group, name, token, repo=None, 4745 branch="master", slot=None): 4746 if repo: 4747 repo_url = 'https://github.com/' + repo 4748 else: 4749 repo_url = None 4750 4751 site_source_control = show_source_control(cmd=cmd, 4752 resource_group_name=resource_group, 4753 name=name, 4754 slot=slot) 4755 if site_source_control: 4756 if not repo_url: 4757 repo_url = site_source_control.repo_url 4758 4759 delete_source_control(cmd=cmd, 4760 resource_group_name=resource_group, 4761 name=name, 4762 slot=slot) 4763 config_source_control(cmd=cmd, 4764 resource_group_name=resource_group, 4765 name=name, 4766 repo_url=repo_url, 4767 repository_type='github', 4768 github_action=True, 4769 branch=branch, 4770 git_token=token, 4771 slot=slot) 4772 4773 4774def _get_workflow_template(github, runtime_string, is_linux): 4775 from github import GithubException 4776 from github.GithubException import BadCredentialsException 4777 4778 file_contents = None 4779 template_repo_path = 'Azure/actions-workflow-templates' 4780 template_file_path = _get_template_file_path(runtime_string=runtime_string, is_linux=is_linux) 4781 4782 try: 4783 template_repo = github.get_repo(template_repo_path) 4784 file_contents = template_repo.get_contents(template_file_path) 4785 except BadCredentialsException: 4786 raise CLIError("Could not authenticate to the repository. Please create a Personal Access Token and use " 4787 "the --token argument. Run 'az webapp deployment github-actions add --help' " 4788 "for more information.") 4789 except GithubException as e: 4790 error_msg = "Encountered GitHub error when retrieving workflow template" 4791 if e.data and e.data['message']: 4792 error_msg += ": {}".format(e.data['message']) 4793 raise CLIError(error_msg) 4794 return file_contents 4795 4796 4797def _fill_workflow_template(content, name, branch, slot, publish_profile, version): 4798 if not slot: 4799 slot = 'production' 4800 4801 content = content.replace('${web-app-name}', name) 4802 content = content.replace('${branch}', branch) 4803 content = content.replace('${slot-name}', slot) 4804 content = content.replace('${azure-webapp-publish-profile-name}', publish_profile) 4805 content = content.replace('${AZURE_WEBAPP_PUBLISH_PROFILE}', publish_profile) 4806 content = content.replace('${dotnet-core-version}', version) 4807 content = content.replace('${java-version}', version) 4808 content = content.replace('${node-version}', version) 4809 content = content.replace('${python-version}', version) 4810 return content 4811 4812 4813def _get_template_file_path(runtime_string, is_linux): 4814 if not runtime_string: 4815 raise CLIError('Unable to retrieve workflow template') 4816 4817 runtime_string = runtime_string.lower() 4818 runtime_stack = runtime_string.split('|')[0] 4819 template_file_path = None 4820 4821 if is_linux: 4822 template_file_path = LINUX_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH.get(runtime_stack, None) 4823 else: 4824 # Handle java naming 4825 if runtime_stack == 'java': 4826 java_container_split = runtime_string.split('|') 4827 if java_container_split and len(java_container_split) >= 2: 4828 if java_container_split[2] == 'tomcat': 4829 runtime_stack = 'tomcat' 4830 elif java_container_split[2] == 'java se': 4831 runtime_stack = 'java' 4832 template_file_path = WINDOWS_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH.get(runtime_stack, None) 4833 4834 if not template_file_path: 4835 raise CLIError('Unable to retrieve workflow template.') 4836 return template_file_path 4837 4838 4839def _add_publish_profile_to_github(cmd, resource_group, name, repo, token, github_actions_secret_name, slot=None): 4840 # Get publish profile with secrets 4841 import requests 4842 4843 logger.warning("Fetching publish profile with secrets for the app '%s'", name) 4844 publish_profile_bytes = _generic_site_operation( 4845 cmd.cli_ctx, resource_group, name, 'list_publishing_profile_xml_with_secrets', 4846 slot, {"format": "WebDeploy"}) 4847 publish_profile = list(publish_profile_bytes) 4848 if publish_profile: 4849 publish_profile = publish_profile[0].decode('ascii') 4850 else: 4851 raise CLIError('Unable to retrieve publish profile.') 4852 4853 # Add publish profile with secrets as a GitHub Actions Secret in the repo 4854 headers = {} 4855 headers['Authorization'] = 'Token {}'.format(token) 4856 headers['Content-Type'] = 'application/json;' 4857 headers['Accept'] = 'application/json;' 4858 4859 public_key_url = "https://api.github.com/repos/{}/actions/secrets/public-key".format(repo) 4860 public_key = requests.get(public_key_url, headers=headers) 4861 if not public_key.ok: 4862 raise CLIError('Request to GitHub for public key failed.') 4863 public_key = public_key.json() 4864 4865 encrypted_github_actions_secret = _encrypt_github_actions_secret(public_key=public_key['key'], 4866 secret_value=str(publish_profile)) 4867 payload = { 4868 "encrypted_value": encrypted_github_actions_secret, 4869 "key_id": public_key['key_id'] 4870 } 4871 4872 store_secret_url = "https://api.github.com/repos/{}/actions/secrets/{}".format(repo, github_actions_secret_name) 4873 stored_secret = requests.put(store_secret_url, data=json.dumps(payload), headers=headers) 4874 if str(stored_secret.status_code)[0] != '2': 4875 raise CLIError('Unable to add publish profile to GitHub. Request status code: %s' % stored_secret.status_code) 4876 4877 4878def _remove_publish_profile_from_github(cmd, resource_group, name, repo, token, github_actions_secret_name, slot=None): 4879 headers = {} 4880 headers['Authorization'] = 'Token {}'.format(token) 4881 4882 import requests 4883 store_secret_url = "https://api.github.com/repos/{}/actions/secrets/{}".format(repo, github_actions_secret_name) 4884 requests.delete(store_secret_url, headers=headers) 4885 4886 4887def _runtime_supports_github_actions(runtime_string, is_linux): 4888 if is_linux: 4889 stacks = get_file_json(RUNTIME_STACKS)['linux'] 4890 else: 4891 stacks = get_file_json(RUNTIME_STACKS)['windows'] 4892 4893 supports = False 4894 for stack in stacks: 4895 if stack['displayName'].lower() == runtime_string.lower(): 4896 if 'github_actions_properties' in stack and stack['github_actions_properties']: 4897 supports = True 4898 return supports 4899 4900 4901def _get_app_runtime_info(cmd, resource_group, name, slot, is_linux): 4902 app_settings = None 4903 app_runtime = None 4904 4905 if is_linux: 4906 app_metadata = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) 4907 app_runtime = getattr(app_metadata, 'linux_fx_version', None) 4908 return _get_app_runtime_info_helper(app_runtime, "", is_linux) 4909 4910 app_metadata = _generic_site_operation(cmd.cli_ctx, resource_group, name, 'list_metadata', slot) 4911 app_metadata_properties = getattr(app_metadata, 'properties', {}) 4912 if 'CURRENT_STACK' in app_metadata_properties: 4913 app_runtime = app_metadata_properties['CURRENT_STACK'] 4914 4915 if app_runtime and app_runtime.lower() == 'node': 4916 app_settings = get_app_settings(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) 4917 for app_setting in app_settings: 4918 if 'name' in app_setting and app_setting['name'] == 'WEBSITE_NODE_DEFAULT_VERSION': 4919 app_runtime_version = app_setting['value'] if 'value' in app_setting else None 4920 if app_runtime_version: 4921 return _get_app_runtime_info_helper(app_runtime, app_runtime_version, is_linux) 4922 elif app_runtime and app_runtime.lower() == 'python': 4923 app_settings = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) 4924 app_runtime_version = getattr(app_settings, 'python_version', '') 4925 return _get_app_runtime_info_helper(app_runtime, app_runtime_version, is_linux) 4926 elif app_runtime and app_runtime.lower() == 'dotnetcore': 4927 app_runtime_version = '3.1' 4928 app_runtime_version = "" 4929 return _get_app_runtime_info_helper(app_runtime, app_runtime_version, is_linux) 4930 elif app_runtime and app_runtime.lower() == 'java': 4931 app_settings = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) 4932 app_runtime_version = "{java_version}, {java_container}, {java_container_version}".format( 4933 java_version=getattr(app_settings, 'java_version', '').lower(), 4934 java_container=getattr(app_settings, 'java_container', '').lower(), 4935 java_container_version=getattr(app_settings, 'java_container_version', '').lower() 4936 ) 4937 return _get_app_runtime_info_helper(app_runtime, app_runtime_version, is_linux) 4938 4939 4940def _get_app_runtime_info_helper(app_runtime, app_runtime_version, is_linux): 4941 if is_linux: 4942 stacks = get_file_json(RUNTIME_STACKS)['linux'] 4943 for stack in stacks: 4944 if 'github_actions_properties' in stack and stack['github_actions_properties']: 4945 if stack['displayName'].lower() == app_runtime.lower(): 4946 return { 4947 "display_name": stack['displayName'], 4948 "github_actions_version": stack['github_actions_properties']['github_actions_version'] 4949 } 4950 else: 4951 stacks = get_file_json(RUNTIME_STACKS)['windows'] 4952 for stack in stacks: 4953 if 'github_actions_properties' in stack and stack['github_actions_properties']: 4954 if (stack['github_actions_properties']['app_runtime'].lower() == app_runtime.lower() and 4955 stack['github_actions_properties']['app_runtime_version'].lower() == 4956 app_runtime_version.lower()): 4957 return { 4958 "display_name": stack['displayName'], 4959 "github_actions_version": stack['github_actions_properties']['github_actions_version'] 4960 } 4961 return None 4962 4963 4964def _encrypt_github_actions_secret(public_key, secret_value): 4965 # Encrypt a Unicode string using the public key 4966 from base64 import b64encode 4967 public_key = public.PublicKey(public_key.encode("utf-8"), encoding.Base64Encoder()) 4968 sealed_box = public.SealedBox(public_key) 4969 encrypted = sealed_box.encrypt(secret_value.encode("utf-8")) 4970 return b64encode(encrypted).decode("utf-8") 4971