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