1#!/usr/local/bin/python3.8
2#
3# Copyright (c) 2018 Yunge Zhu, <yungez@microsoft.com>
4#
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11DOCUMENTATION = '''
12---
13module: azure_rm_webapp
14version_added: "0.1.2"
15short_description: Manage Web App instances
16description:
17    - Create, update and delete instance of Web App.
18
19options:
20    resource_group:
21        description:
22            - Name of the resource group to which the resource belongs.
23        required: True
24    name:
25        description:
26            - Unique name of the app to create or update. To create or update a deployment slot, use the {slot} parameter.
27        required: True
28
29    location:
30        description:
31            - Resource location. If not set, location from the resource group will be used as default.
32
33    plan:
34        description:
35            - App service plan. Required for creation.
36            - Can be name of existing app service plan in same resource group as web app.
37            - Can be the resource ID of an existing app service plan. For example
38              /subscriptions/<subs_id>/resourceGroups/<resource_group>/providers/Microsoft.Web/serverFarms/<plan_name>.
39            - Can be a dict containing five parameters, defined below.
40            - C(name), name of app service plan.
41            - C(resource_group), resource group of the app service plan.
42            - C(sku), SKU of app service plan, allowed values listed on U(https://azure.microsoft.com/en-us/pricing/details/app-service/linux/).
43            - C(is_linux), whether or not the app service plan is Linux. defaults to C(False).
44            - C(number_of_workers), number of workers for app service plan.
45
46    frameworks:
47        description:
48            - Set of run time framework settings. Each setting is a dictionary.
49            - See U(https://docs.microsoft.com/en-us/azure/app-service/app-service-web-overview) for more info.
50        suboptions:
51            name:
52                description:
53                    - Name of the framework.
54                    - Supported framework list for Windows web app and Linux web app is different.
55                    - Windows web apps support C(java), C(net_framework), C(php), C(python), and C(node) from June 2018.
56                    - Windows web apps support multiple framework at the same time.
57                    - Linux web apps support C(java), C(ruby), C(php), C(dotnetcore), and C(node) from June 2018.
58                    - Linux web apps support only one framework.
59                    - Java framework is mutually exclusive with others.
60                choices:
61                    - java
62                    - net_framework
63                    - php
64                    - python
65                    - ruby
66                    - dotnetcore
67                    - node
68            version:
69                description:
70                    - Version of the framework. For Linux web app supported value, see U(https://aka.ms/linux-stacks) for more info.
71                    - C(net_framework) supported value sample, C(v4.0) for .NET 4.6 and C(v3.0) for .NET 3.5.
72                    - C(php) supported value sample, C(5.5), C(5.6), C(7.0).
73                    - C(python) supported value sample, C(5.5), C(5.6), C(7.0).
74                    - C(node) supported value sample, C(6.6), C(6.9).
75                    - C(dotnetcore) supported value sample, C(1.0), C(1.1), C(1.2).
76                    - C(ruby) supported value sample, C(2.3).
77                    - C(java) supported value sample, C(1.9) for Windows web app. C(1.8) for Linux web app.
78            settings:
79                description:
80                    - List of settings of the framework.
81                suboptions:
82                    java_container:
83                        description:
84                            - Name of Java container.
85                            - Supported only when I(frameworks=java). Sample values C(Tomcat), C(Jetty).
86                    java_container_version:
87                        description:
88                            - Version of Java container.
89                            - Supported only when I(frameworks=java).
90                            - Sample values for C(Tomcat), C(8.0), C(8.5), C(9.0). For C(Jetty,), C(9.1), C(9.3).
91
92    container_settings:
93        description:
94            - Web app container settings.
95        suboptions:
96            name:
97                description:
98                    - Name of the container, for example C(imagename:tag).
99                    - To create a multi-container app, the name should be 'COMPOSE|' or 'KUBE|' followed by base64 encoded configuration.
100            registry_server_url:
101                description:
102                    - Container registry server URL, for example C(mydockerregistry.io).
103            registry_server_user:
104                description:
105                    - The container registry server user name.
106            registry_server_password:
107                description:
108                    - The container registry server password.
109
110    scm_type:
111        description:
112            - Repository type of deployment source, for example C(LocalGit), C(GitHub).
113            - List of supported values maintained at U(https://docs.microsoft.com/en-us/rest/api/appservice/webapps/createorupdate#scmtype).
114
115    deployment_source:
116        description:
117            - Deployment source for git.
118        suboptions:
119            url:
120                description:
121                    - Repository url of deployment source.
122
123            branch:
124                description:
125                    - The branch name of the repository.
126    startup_file:
127        description:
128            - The web's startup file.
129            - Used only for Linux web apps.
130
131    client_affinity_enabled:
132        description:
133            - Whether or not to send session affinity cookies, which route client requests in the same session to the same instance.
134        type: bool
135        default: True
136
137    https_only:
138        description:
139            - Configures web site to accept only https requests.
140        type: bool
141
142    dns_registration:
143        description:
144            - Whether or not the web app hostname is registered with DNS on creation. Set to C(false) to register.
145        type: bool
146
147    skip_custom_domain_verification:
148        description:
149            - Whether or not to skip verification of custom (non *.azurewebsites.net) domains associated with web app. Set to C(true) to skip.
150        type: bool
151
152    ttl_in_seconds:
153        description:
154            - Time to live in seconds for web app default domain name.
155
156    app_settings:
157        description:
158            - Configure web app application settings. Suboptions are in key value pair format.
159
160    purge_app_settings:
161        description:
162            - Purge any existing application settings. Replace web app application settings with app_settings.
163        type: bool
164        default: False
165
166    app_state:
167        description:
168            - Start/Stop/Restart the web app.
169        type: str
170        choices:
171            - started
172            - stopped
173            - restarted
174        default: started
175
176    state:
177        description:
178            - State of the Web App.
179            - Use C(present) to create or update a Web App and C(absent) to delete it.
180        default: present
181        choices:
182            - absent
183            - present
184
185extends_documentation_fragment:
186    - azure.azcollection.azure
187    - azure.azcollection.azure_tags
188
189author:
190    - Yunge Zhu (@yungezz)
191
192'''
193
194EXAMPLES = '''
195    - name: Create a windows web app with non-exist app service plan
196      azure_rm_webapp:
197        resource_group: myResourceGroup
198        name: myWinWebapp
199        plan:
200          resource_group: myAppServicePlan_rg
201          name: myAppServicePlan
202          is_linux: false
203          sku: S1
204
205    - name: Create a docker web app with some app settings, with docker image
206      azure_rm_webapp:
207        resource_group: myResourceGroup
208        name: myDockerWebapp
209        plan:
210          resource_group: myAppServicePlan_rg
211          name: myAppServicePlan
212          is_linux: true
213          sku: S1
214          number_of_workers: 2
215        app_settings:
216          testkey: testvalue
217          testkey2: testvalue2
218        container_settings:
219          name: ansible/ansible:ubuntu1404
220
221    - name: Create a docker web app with private acr registry
222      azure_rm_webapp:
223        resource_group: myResourceGroup
224        name: myDockerWebapp
225        plan: myAppServicePlan
226        app_settings:
227          testkey: testvalue
228        container_settings:
229          name: ansible/ubuntu1404
230          registry_server_url: myregistry.io
231          registry_server_user: user
232          registry_server_password: pass
233
234    - name: Create a multi-container web app
235      azure_rm_webapp:
236        resource_group: myResourceGroup
237        name: myMultiContainerWebapp
238        plan: myAppServicePlan
239        app_settings:
240          testkey: testvalue
241        container_settings:
242          name: "COMPOSE|{{ lookup('file', 'docker-compose.yml') | b64encode }}"
243
244    - name: Create a linux web app with Node 6.6 framework
245      azure_rm_webapp:
246        resource_group: myResourceGroup
247        name: myLinuxWebapp
248        plan:
249          resource_group: myAppServicePlan_rg
250          name: myAppServicePlan
251        app_settings:
252          testkey: testvalue
253        frameworks:
254          - name: "node"
255            version: "6.6"
256
257    - name: Create a windows web app with node, php
258      azure_rm_webapp:
259        resource_group: myResourceGroup
260        name: myWinWebapp
261        plan:
262          resource_group: myAppServicePlan_rg
263          name: myAppServicePlan
264        app_settings:
265          testkey: testvalue
266        frameworks:
267          - name: "node"
268            version: 6.6
269          - name: "php"
270            version: "7.0"
271
272    - name: Create a stage deployment slot for an existing web app
273      azure_rm_webapp:
274        resource_group: myResourceGroup
275        name: myWebapp/slots/stage
276        plan:
277          resource_group: myAppServicePlan_rg
278          name: myAppServicePlan
279        app_settings:
280          testkey:testvalue
281
282    - name: Create a linux web app with java framework
283      azure_rm_webapp:
284        resource_group: myResourceGroup
285        name: myLinuxWebapp
286        plan:
287          resource_group: myAppServicePlan_rg
288          name: myAppServicePlan
289        app_settings:
290          testkey: testvalue
291        frameworks:
292          - name: "java"
293            version: "8"
294            settings:
295              java_container: "Tomcat"
296              java_container_version: "8.5"
297'''
298
299RETURN = '''
300azure_webapp:
301    description:
302        - ID of current web app.
303    returned: always
304    type: str
305    sample: "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Web/sites/myWebApp"
306'''
307
308import time
309from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase
310
311try:
312    from msrestazure.azure_exceptions import CloudError
313    from msrest.polling import LROPoller
314    from msrest.serialization import Model
315    from azure.mgmt.web.models import (
316        site_config, app_service_plan, Site,
317        AppServicePlan, SkuDescription, NameValuePair
318    )
319except ImportError:
320    # This is handled in azure_rm_common
321    pass
322
323container_settings_spec = dict(
324    name=dict(type='str', required=True),
325    registry_server_url=dict(type='str'),
326    registry_server_user=dict(type='str'),
327    registry_server_password=dict(type='str', no_log=True)
328)
329
330deployment_source_spec = dict(
331    url=dict(type='str'),
332    branch=dict(type='str')
333)
334
335
336framework_settings_spec = dict(
337    java_container=dict(type='str', required=True),
338    java_container_version=dict(type='str', required=True)
339)
340
341
342framework_spec = dict(
343    name=dict(
344        type='str',
345        required=True,
346        choices=['net_framework', 'java', 'php', 'node', 'python', 'dotnetcore', 'ruby']),
347    version=dict(type='str', required=True),
348    settings=dict(type='dict', options=framework_settings_spec)
349)
350
351
352def _normalize_sku(sku):
353    if sku is None:
354        return sku
355
356    sku = sku.upper()
357    if sku == 'FREE':
358        return 'F1'
359    elif sku == 'SHARED':
360        return 'D1'
361    return sku
362
363
364def get_sku_name(tier):
365    tier = tier.upper()
366    if tier == 'F1' or tier == "FREE":
367        return 'FREE'
368    elif tier == 'D1' or tier == "SHARED":
369        return 'SHARED'
370    elif tier in ['B1', 'B2', 'B3', 'BASIC']:
371        return 'BASIC'
372    elif tier in ['S1', 'S2', 'S3']:
373        return 'STANDARD'
374    elif tier in ['P1', 'P2', 'P3']:
375        return 'PREMIUM'
376    elif tier in ['P1V2', 'P2V2', 'P3V2']:
377        return 'PREMIUMV2'
378    else:
379        return None
380
381
382def appserviceplan_to_dict(plan):
383    return dict(
384        id=plan.id,
385        name=plan.name,
386        kind=plan.kind,
387        location=plan.location,
388        reserved=plan.reserved,
389        is_linux=plan.reserved,
390        provisioning_state=plan.provisioning_state,
391        tags=plan.tags if plan.tags else None
392    )
393
394
395def webapp_to_dict(webapp):
396    return dict(
397        id=webapp.id,
398        name=webapp.name,
399        location=webapp.location,
400        client_cert_enabled=webapp.client_cert_enabled,
401        enabled=webapp.enabled,
402        reserved=webapp.reserved,
403        client_affinity_enabled=webapp.client_affinity_enabled,
404        server_farm_id=webapp.server_farm_id,
405        host_names_disabled=webapp.host_names_disabled,
406        https_only=webapp.https_only if hasattr(webapp, 'https_only') else None,
407        skip_custom_domain_verification=webapp.skip_custom_domain_verification if hasattr(webapp, 'skip_custom_domain_verification') else None,
408        ttl_in_seconds=webapp.ttl_in_seconds if hasattr(webapp, 'ttl_in_seconds') else None,
409        state=webapp.state,
410        tags=webapp.tags if webapp.tags else None
411    )
412
413
414class Actions:
415    CreateOrUpdate, UpdateAppSettings, Delete = range(3)
416
417
418class AzureRMWebApps(AzureRMModuleBase):
419    """Configuration class for an Azure RM Web App resource"""
420
421    def __init__(self):
422        self.module_arg_spec = dict(
423            resource_group=dict(
424                type='str',
425                required=True
426            ),
427            name=dict(
428                type='str',
429                required=True
430            ),
431            location=dict(
432                type='str'
433            ),
434            plan=dict(
435                type='raw'
436            ),
437            frameworks=dict(
438                type='list',
439                elements='dict',
440                options=framework_spec
441            ),
442            container_settings=dict(
443                type='dict',
444                options=container_settings_spec
445            ),
446            scm_type=dict(
447                type='str',
448            ),
449            deployment_source=dict(
450                type='dict',
451                options=deployment_source_spec
452            ),
453            startup_file=dict(
454                type='str'
455            ),
456            client_affinity_enabled=dict(
457                type='bool',
458                default=True
459            ),
460            dns_registration=dict(
461                type='bool'
462            ),
463            https_only=dict(
464                type='bool'
465            ),
466            skip_custom_domain_verification=dict(
467                type='bool'
468            ),
469            ttl_in_seconds=dict(
470                type='int'
471            ),
472            app_settings=dict(
473                type='dict'
474            ),
475            purge_app_settings=dict(
476                type='bool',
477                default=False
478            ),
479            app_state=dict(
480                type='str',
481                choices=['started', 'stopped', 'restarted'],
482                default='started'
483            ),
484            state=dict(
485                type='str',
486                default='present',
487                choices=['present', 'absent']
488            )
489        )
490
491        mutually_exclusive = [['container_settings', 'frameworks']]
492
493        self.resource_group = None
494        self.name = None
495        self.location = None
496
497        # update in create_or_update as parameters
498        self.client_affinity_enabled = True
499        self.dns_registration = None
500        self.skip_custom_domain_verification = None
501        self.ttl_in_seconds = None
502        self.https_only = None
503
504        self.tags = None
505
506        # site config, e.g app settings, ssl
507        self.site_config = dict()
508        self.app_settings = dict()
509        self.app_settings_strDic = None
510
511        # app service plan
512        self.plan = None
513
514        # siteSourceControl
515        self.deployment_source = dict()
516
517        # site, used at level creation, or update. e.g windows/linux, client_affinity etc first level args
518        self.site = None
519
520        # property for internal usage, not used for sdk
521        self.container_settings = None
522
523        self.purge_app_settings = False
524        self.app_state = 'started'
525
526        self.results = dict(
527            changed=False,
528            id=None,
529        )
530        self.state = None
531        self.to_do = []
532
533        self.frameworks = None
534
535        # set site_config value from kwargs
536        self.site_config_updatable_properties = ["net_framework_version",
537                                                 "java_version",
538                                                 "php_version",
539                                                 "python_version",
540                                                 "scm_type"]
541
542        # updatable_properties
543        self.updatable_properties = ["client_affinity_enabled",
544                                     "force_dns_registration",
545                                     "https_only",
546                                     "skip_custom_domain_verification",
547                                     "ttl_in_seconds"]
548
549        self.supported_linux_frameworks = ['ruby', 'php', 'dotnetcore', 'node', 'java']
550        self.supported_windows_frameworks = ['net_framework', 'php', 'python', 'node', 'java']
551
552        super(AzureRMWebApps, self).__init__(derived_arg_spec=self.module_arg_spec,
553                                             mutually_exclusive=mutually_exclusive,
554                                             supports_check_mode=True,
555                                             supports_tags=True)
556
557    def exec_module(self, **kwargs):
558        """Main module execution method"""
559
560        for key in list(self.module_arg_spec.keys()) + ['tags']:
561            if hasattr(self, key):
562                setattr(self, key, kwargs[key])
563            elif kwargs[key] is not None:
564                if key == "scm_type":
565                    self.site_config[key] = kwargs[key]
566
567        old_response = None
568        response = None
569        to_be_updated = False
570
571        # set location
572        resource_group = self.get_resource_group(self.resource_group)
573        if not self.location:
574            self.location = resource_group.location
575
576        # get existing web app
577        old_response = self.get_webapp()
578
579        if old_response:
580            self.results['id'] = old_response['id']
581
582        if self.state == 'present':
583            if not self.plan and not old_response:
584                self.fail("Please specify plan for newly created web app.")
585
586            if not self.plan:
587                self.plan = old_response['server_farm_id']
588
589            self.plan = self.parse_resource_to_dict(self.plan)
590
591            # get app service plan
592            is_linux = False
593            old_plan = self.get_app_service_plan()
594            if old_plan:
595                is_linux = old_plan['reserved']
596            else:
597                is_linux = self.plan['is_linux'] if 'is_linux' in self.plan else False
598
599            if self.frameworks:
600                # java is mutually exclusive with other frameworks
601                if len(self.frameworks) > 1 and any(f['name'] == 'java' for f in self.frameworks):
602                    self.fail('Java is mutually exclusive with other frameworks.')
603
604                if is_linux:
605                    if len(self.frameworks) != 1:
606                        self.fail('Can specify one framework only for Linux web app.')
607
608                    if self.frameworks[0]['name'] not in self.supported_linux_frameworks:
609                        self.fail('Unsupported framework {0} for Linux web app.'.format(self.frameworks[0]['name']))
610
611                    self.site_config['linux_fx_version'] = (self.frameworks[0]['name'] + '|' + self.frameworks[0]['version']).upper()
612
613                    if self.frameworks[0]['name'] == 'java':
614                        if self.frameworks[0]['version'] != '8':
615                            self.fail("Linux web app only supports java 8.")
616                        if self.frameworks[0]['settings'] and self.frameworks[0]['settings']['java_container'].lower() != 'tomcat':
617                            self.fail("Linux web app only supports tomcat container.")
618
619                        if self.frameworks[0]['settings'] and self.frameworks[0]['settings']['java_container'].lower() == 'tomcat':
620                            self.site_config['linux_fx_version'] = 'TOMCAT|' + self.frameworks[0]['settings']['java_container_version'] + '-jre8'
621                        else:
622                            self.site_config['linux_fx_version'] = 'JAVA|8-jre8'
623                else:
624                    for fx in self.frameworks:
625                        if fx.get('name') not in self.supported_windows_frameworks:
626                            self.fail('Unsupported framework {0} for Windows web app.'.format(fx.get('name')))
627                        else:
628                            self.site_config[fx.get('name') + '_version'] = fx.get('version')
629
630                        if 'settings' in fx and fx['settings'] is not None:
631                            for key, value in fx['settings'].items():
632                                self.site_config[key] = value
633
634            if not self.app_settings:
635                self.app_settings = dict()
636
637            if self.container_settings:
638                linux_fx_version = 'DOCKER|'
639
640                if self.container_settings.get('registry_server_url'):
641                    self.app_settings['DOCKER_REGISTRY_SERVER_URL'] = 'https://' + self.container_settings['registry_server_url']
642
643                    linux_fx_version += self.container_settings['registry_server_url'] + '/'
644
645                linux_fx_version += self.container_settings['name']
646
647                # Use given name as is if it starts with allowed values of multi-container application
648                if self.container_settings['name'].startswith('COMPOSE|') or self.container_settings['name'].startswith('KUBE|'):
649                    linux_fx_version = self.container_settings['name']
650
651                self.site_config['linux_fx_version'] = linux_fx_version
652
653                if self.container_settings.get('registry_server_user'):
654                    self.app_settings['DOCKER_REGISTRY_SERVER_USERNAME'] = self.container_settings['registry_server_user']
655
656                if self.container_settings.get('registry_server_password'):
657                    self.app_settings['DOCKER_REGISTRY_SERVER_PASSWORD'] = self.container_settings['registry_server_password']
658
659            # init site
660            self.site = Site(location=self.location, site_config=self.site_config)
661
662            if self.https_only is not None:
663                self.site.https_only = self.https_only
664
665            if self.client_affinity_enabled:
666                self.site.client_affinity_enabled = self.client_affinity_enabled
667
668            # check if the web app already present in the resource group
669            if not old_response:
670                self.log("Web App instance doesn't exist")
671
672                to_be_updated = True
673                self.to_do.append(Actions.CreateOrUpdate)
674                self.site.tags = self.tags
675
676                # service plan is required for creation
677                if not self.plan:
678                    self.fail("Please specify app service plan in plan parameter.")
679
680                if not old_plan:
681                    # no existing service plan, create one
682                    if (not self.plan.get('name') or not self.plan.get('sku')):
683                        self.fail('Please specify name, is_linux, sku in plan')
684
685                    if 'location' not in self.plan:
686                        plan_resource_group = self.get_resource_group(self.plan['resource_group'])
687                        self.plan['location'] = plan_resource_group.location
688
689                    old_plan = self.create_app_service_plan()
690
691                self.site.server_farm_id = old_plan['id']
692
693                # if linux, setup startup_file
694                if old_plan['is_linux']:
695                    if hasattr(self, 'startup_file'):
696                        self.site_config['app_command_line'] = self.startup_file
697
698                # set app setting
699                if self.app_settings:
700                    app_settings = []
701                    for key in self.app_settings.keys():
702                        app_settings.append(NameValuePair(name=key, value=self.app_settings[key]))
703
704                    self.site_config['app_settings'] = app_settings
705            else:
706                # existing web app, do update
707                self.log("Web App instance already exists")
708
709                self.log('Result: {0}'.format(old_response))
710
711                update_tags, self.site.tags = self.update_tags(old_response.get('tags', None))
712
713                if update_tags:
714                    to_be_updated = True
715
716                # check if root level property changed
717                if self.is_updatable_property_changed(old_response):
718                    to_be_updated = True
719                    self.to_do.append(Actions.CreateOrUpdate)
720
721                # check if site_config changed
722                old_config = self.get_webapp_configuration()
723
724                if self.is_site_config_changed(old_config):
725                    to_be_updated = True
726                    self.to_do.append(Actions.CreateOrUpdate)
727
728                # check if linux_fx_version changed
729                if old_config.linux_fx_version != self.site_config.get('linux_fx_version', ''):
730                    to_be_updated = True
731                    self.to_do.append(Actions.CreateOrUpdate)
732
733                self.app_settings_strDic = self.list_app_settings()
734
735                # purge existing app_settings:
736                if self.purge_app_settings:
737                    to_be_updated = True
738                    self.app_settings_strDic = dict()
739                    self.to_do.append(Actions.UpdateAppSettings)
740
741                # check if app settings changed
742                if self.purge_app_settings or self.is_app_settings_changed():
743                    to_be_updated = True
744                    self.to_do.append(Actions.UpdateAppSettings)
745
746                    if self.app_settings:
747                        for key in self.app_settings.keys():
748                            self.app_settings_strDic[key] = self.app_settings[key]
749
750        elif self.state == 'absent':
751            if old_response:
752                self.log("Delete Web App instance")
753                self.results['changed'] = True
754
755                if self.check_mode:
756                    return self.results
757
758                self.delete_webapp()
759
760                self.log('Web App instance deleted')
761
762            else:
763                self.fail("Web app {0} not exists.".format(self.name))
764
765        if to_be_updated:
766            self.log('Need to Create/Update web app')
767            self.results['changed'] = True
768
769            if self.check_mode:
770                return self.results
771
772            if Actions.CreateOrUpdate in self.to_do:
773                response = self.create_update_webapp()
774
775                self.results['id'] = response['id']
776
777            if Actions.UpdateAppSettings in self.to_do:
778                update_response = self.update_app_settings()
779                self.results['id'] = update_response.id
780
781        webapp = None
782        if old_response:
783            webapp = old_response
784        if response:
785            webapp = response
786
787        if webapp:
788            if (webapp['state'] != 'Stopped' and self.app_state == 'stopped') or \
789               (webapp['state'] != 'Running' and self.app_state == 'started') or \
790               self.app_state == 'restarted':
791
792                self.results['changed'] = True
793                if self.check_mode:
794                    return self.results
795
796                self.set_webapp_state(self.app_state)
797
798        return self.results
799
800    # compare existing web app with input, determine weather it's update operation
801    def is_updatable_property_changed(self, existing_webapp):
802        for property_name in self.updatable_properties:
803            if hasattr(self, property_name) and getattr(self, property_name) is not None and \
804                    getattr(self, property_name) != existing_webapp.get(property_name, None):
805                return True
806
807        return False
808
809    # compare xxx_version
810    def is_site_config_changed(self, existing_config):
811        for fx_version in self.site_config_updatable_properties:
812            if self.site_config.get(fx_version):
813                if not getattr(existing_config, fx_version) or \
814                        getattr(existing_config, fx_version).upper() != self.site_config.get(fx_version).upper():
815                    return True
816
817        return False
818
819    # comparing existing app setting with input, determine whether it's changed
820    def is_app_settings_changed(self):
821        if self.app_settings:
822            if self.app_settings_strDic:
823                for key in self.app_settings.keys():
824                    if self.app_settings[key] != self.app_settings_strDic.get(key, None):
825                        return True
826            else:
827                return True
828        return False
829
830    # comparing deployment source with input, determine wheather it's changed
831    def is_deployment_source_changed(self, existing_webapp):
832        if self.deployment_source:
833            if self.deployment_source.get('url') \
834                    and self.deployment_source['url'] != existing_webapp.get('site_source_control')['url']:
835                return True
836
837            if self.deployment_source.get('branch') \
838                    and self.deployment_source['branch'] != existing_webapp.get('site_source_control')['branch']:
839                return True
840
841        return False
842
843    def create_update_webapp(self):
844        '''
845        Creates or updates Web App with the specified configuration.
846
847        :return: deserialized Web App instance state dictionary
848        '''
849        self.log(
850            "Creating / Updating the Web App instance {0}".format(self.name))
851
852        try:
853            skip_dns_registration = self.dns_registration
854            force_dns_registration = None if self.dns_registration is None else not self.dns_registration
855
856            response = self.web_client.web_apps.create_or_update(resource_group_name=self.resource_group,
857                                                                 name=self.name,
858                                                                 site_envelope=self.site,
859                                                                 skip_dns_registration=skip_dns_registration,
860                                                                 skip_custom_domain_verification=self.skip_custom_domain_verification,
861                                                                 force_dns_registration=force_dns_registration,
862                                                                 ttl_in_seconds=self.ttl_in_seconds)
863            if isinstance(response, LROPoller):
864                response = self.get_poller_result(response)
865
866        except CloudError as exc:
867            self.log('Error attempting to create the Web App instance.')
868            self.fail(
869                "Error creating the Web App instance: {0}".format(str(exc)))
870        return webapp_to_dict(response)
871
872    def delete_webapp(self):
873        '''
874        Deletes specified Web App instance in the specified subscription and resource group.
875
876        :return: True
877        '''
878        self.log("Deleting the Web App instance {0}".format(self.name))
879        try:
880            response = self.web_client.web_apps.delete(resource_group_name=self.resource_group,
881                                                       name=self.name)
882        except CloudError as e:
883            self.log('Error attempting to delete the Web App instance.')
884            self.fail(
885                "Error deleting the Web App instance: {0}".format(str(e)))
886
887        return True
888
889    def get_webapp(self):
890        '''
891        Gets the properties of the specified Web App.
892
893        :return: deserialized Web App instance state dictionary
894        '''
895        self.log(
896            "Checking if the Web App instance {0} is present".format(self.name))
897
898        response = None
899
900        try:
901            response = self.web_client.web_apps.get(resource_group_name=self.resource_group,
902                                                    name=self.name)
903
904            # Newer SDK versions (0.40.0+) seem to return None if it doesn't exist instead of raising CloudError
905            if response is not None:
906                self.log("Response : {0}".format(response))
907                self.log("Web App instance : {0} found".format(response.name))
908                return webapp_to_dict(response)
909
910        except CloudError as ex:
911            pass
912
913        self.log("Didn't find web app {0} in resource group {1}".format(
914            self.name, self.resource_group))
915
916        return False
917
918    def get_app_service_plan(self):
919        '''
920        Gets app service plan
921        :return: deserialized app service plan dictionary
922        '''
923        self.log("Get App Service Plan {0}".format(self.plan['name']))
924
925        try:
926            response = self.web_client.app_service_plans.get(
927                resource_group_name=self.plan['resource_group'],
928                name=self.plan['name'])
929
930            # Newer SDK versions (0.40.0+) seem to return None if it doesn't exist instead of raising CloudError
931            if response is not None:
932                self.log("Response : {0}".format(response))
933                self.log("App Service Plan : {0} found".format(response.name))
934
935                return appserviceplan_to_dict(response)
936        except CloudError as ex:
937            pass
938
939        self.log("Didn't find app service plan {0} in resource group {1}".format(
940            self.plan['name'], self.plan['resource_group']))
941
942        return False
943
944    def create_app_service_plan(self):
945        '''
946        Creates app service plan
947        :return: deserialized app service plan dictionary
948        '''
949        self.log("Create App Service Plan {0}".format(self.plan['name']))
950
951        try:
952            # normalize sku
953            sku = _normalize_sku(self.plan['sku'])
954
955            sku_def = SkuDescription(tier=get_sku_name(
956                sku), name=sku, capacity=(self.plan.get('number_of_workers', None)))
957            plan_def = AppServicePlan(
958                location=self.plan['location'], app_service_plan_name=self.plan['name'], sku=sku_def, reserved=(self.plan.get('is_linux', None)))
959
960            poller = self.web_client.app_service_plans.create_or_update(
961                self.plan['resource_group'], self.plan['name'], plan_def)
962
963            if isinstance(poller, LROPoller):
964                response = self.get_poller_result(poller)
965
966            self.log("Response : {0}".format(response))
967
968            return appserviceplan_to_dict(response)
969        except CloudError as ex:
970            self.fail("Failed to create app service plan {0} in resource group {1}: {2}".format(
971                self.plan['name'], self.plan['resource_group'], str(ex)))
972
973    def list_app_settings(self):
974        '''
975        List application settings
976        :return: deserialized list response
977        '''
978        self.log("List application setting")
979
980        try:
981
982            response = self.web_client.web_apps.list_application_settings(
983                resource_group_name=self.resource_group, name=self.name)
984            self.log("Response : {0}".format(response))
985
986            return response.properties
987        except CloudError as ex:
988            self.fail("Failed to list application settings for web app {0} in resource group {1}: {2}".format(
989                self.name, self.resource_group, str(ex)))
990
991    def update_app_settings(self):
992        '''
993        Update application settings
994        :return: deserialized updating response
995        '''
996        self.log("Update application setting")
997
998        try:
999            response = self.web_client.web_apps.update_application_settings(
1000                resource_group_name=self.resource_group, name=self.name, properties=self.app_settings_strDic)
1001            self.log("Response : {0}".format(response))
1002
1003            return response
1004        except CloudError as ex:
1005            self.fail("Failed to update application settings for web app {0} in resource group {1}: {2}".format(
1006                self.name, self.resource_group, str(ex)))
1007
1008    def create_or_update_source_control(self):
1009        '''
1010        Update site source control
1011        :return: deserialized updating response
1012        '''
1013        self.log("Update site source control")
1014
1015        if self.deployment_source is None:
1016            return False
1017
1018        self.deployment_source['is_manual_integration'] = False
1019        self.deployment_source['is_mercurial'] = False
1020
1021        try:
1022            response = self.web_client.web_client.create_or_update_source_control(
1023                self.resource_group, self.name, self.deployment_source)
1024            self.log("Response : {0}".format(response))
1025
1026            return response.as_dict()
1027        except CloudError as ex:
1028            self.fail("Failed to update site source control for web app {0} in resource group {1}".format(
1029                self.name, self.resource_group))
1030
1031    def get_webapp_configuration(self):
1032        '''
1033        Get  web app configuration
1034        :return: deserialized  web app configuration response
1035        '''
1036        self.log("Get web app configuration")
1037
1038        try:
1039
1040            response = self.web_client.web_apps.get_configuration(
1041                resource_group_name=self.resource_group, name=self.name)
1042            self.log("Response : {0}".format(response))
1043
1044            return response
1045        except CloudError as ex:
1046            self.log("Failed to get configuration for web app {0} in resource group {1}: {2}".format(
1047                self.name, self.resource_group, str(ex)))
1048
1049            return False
1050
1051    def set_webapp_state(self, appstate):
1052        '''
1053        Start/stop/restart web app
1054        :return: deserialized updating response
1055        '''
1056        try:
1057            if appstate == 'started':
1058                response = self.web_client.web_apps.start(resource_group_name=self.resource_group, name=self.name)
1059            elif appstate == 'stopped':
1060                response = self.web_client.web_apps.stop(resource_group_name=self.resource_group, name=self.name)
1061            elif appstate == 'restarted':
1062                response = self.web_client.web_apps.restart(resource_group_name=self.resource_group, name=self.name)
1063            else:
1064                self.fail("Invalid web app state {0}".format(appstate))
1065
1066            self.log("Response : {0}".format(response))
1067
1068            return response
1069        except CloudError as ex:
1070            request_id = ex.request_id if ex.request_id else ''
1071            self.log("Failed to {0} web app {1} in resource group {2}, request_id {3} - {4}".format(
1072                appstate, self.name, self.resource_group, request_id, str(ex)))
1073
1074
1075def main():
1076    """Main execution"""
1077    AzureRMWebApps()
1078
1079
1080if __name__ == '__main__':
1081    main()
1082