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