#!/usr/bin/python # # Copyright (c) 2016 Thomas Stringer, # # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = ''' --- module: azure_rm_functionapp version_added: "2.4" short_description: Manage Azure Function Apps description: - Create, update or delete an Azure Function App options: resource_group: description: - Name of resource group required: true aliases: - resource_group_name name: description: - Name of the Azure Function App required: true location: description: - Valid Azure location. Defaults to location of the resource group. storage_account: description: - Name of the storage account to use. required: true aliases: - storage - storage_account_name app_settings: description: - Dictionary containing application settings state: description: - Assert the state of the Function App. Use 'present' to create or update a Function App and 'absent' to delete. default: present choices: - absent - present extends_documentation_fragment: - azure - azure_tags author: - "Thomas Stringer (@tstringer)" ''' EXAMPLES = ''' - name: create function app azure_rm_functionapp: resource_group: ansible-rg name: myfunctionapp storage_account: mystorageaccount - name: create a function app with app settings azure_rm_functionapp: resource_group: ansible-rg name: myfunctionapp storage_account: mystorageaccount app_settings: setting1: value1 setting2: value2 - name: delete a function app azure_rm_functionapp: name: myfunctionapp state: absent ''' RETURN = ''' state: description: Current state of the Azure Function App returned: success type: dict example: id: /subscriptions/.../resourceGroups/ansible-rg/providers/Microsoft.Web/sites/myfunctionapp name: myfunctionapp kind: functionapp location: East US type: Microsoft.Web/sites state: Running host_names: - myfunctionapp.azurewebsites.net repository_site_name: myfunctionapp usage_state: Normal enabled: true enabled_host_names: - myfunctionapp.azurewebsites.net - myfunctionapp.scm.azurewebsites.net availability_state: Normal host_name_ssl_states: - name: myfunctionapp.azurewebsites.net ssl_state: Disabled host_type: Standard - name: myfunctionapp.scm.azurewebsites.net ssl_state: Disabled host_type: Repository server_farm_id: /subscriptions/.../resourceGroups/ansible-rg/providers/Microsoft.Web/serverfarms/EastUSPlan reserved: false last_modified_time_utc: 2017-08-22T18:54:01.190Z scm_site_also_stopped: false client_affinity_enabled: true client_cert_enabled: false host_names_disabled: false outbound_ip_addresses: ............ container_size: 1536 daily_memory_time_quota: 0 resource_group: ansible-rg default_host_name: myfunctionapp.azurewebsites.net ''' # NOQA from ansible.module_utils.azure_rm_common import AzureRMModuleBase try: from msrestazure.azure_exceptions import CloudError from azure.mgmt.web.models import Site, SiteConfig, NameValuePair, SiteSourceControl from azure.mgmt.resource.resources import ResourceManagementClient except ImportError: # This is handled in azure_rm_common pass class AzureRMFunctionApp(AzureRMModuleBase): def __init__(self): self.module_arg_spec = dict( resource_group=dict(type='str', required=True, aliases=['resource_group_name']), name=dict(type='str', required=True), state=dict(type='str', default='present', choices=['present', 'absent']), location=dict(type='str'), storage_account=dict( type='str', aliases=['storage', 'storage_account_name'] ), app_settings=dict(type='dict') ) self.results = dict( changed=False, state=dict() ) self.resource_group = None self.name = None self.state = None self.location = None self.storage_account = None self.app_settings = None required_if = [('state', 'present', ['storage_account'])] super(AzureRMFunctionApp, self).__init__( self.module_arg_spec, supports_check_mode=True, required_if=required_if ) def exec_module(self, **kwargs): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) if self.app_settings is None: self.app_settings = dict() try: resource_group = self.rm_client.resource_groups.get(self.resource_group) except CloudError: self.fail('Unable to retrieve resource group') self.location = self.location or resource_group.location try: function_app = self.web_client.web_apps.get( resource_group_name=self.resource_group, name=self.name ) exists = True except CloudError as exc: exists = False if self.state == 'absent': if exists: if self.check_mode: self.results['changed'] = True return self.results try: self.web_client.web_apps.delete( resource_group_name=self.resource_group, name=self.name ) self.results['changed'] = True except CloudError as exc: self.fail('Failure while deleting web app: {}'.format(exc)) else: self.results['changed'] = False else: if not exists: function_app = Site( location=self.location, kind='functionapp', site_config=SiteConfig( app_settings=self.aggregated_app_settings(), scm_type='LocalGit' ) ) self.results['changed'] = True else: self.results['changed'], function_app = self.update(function_app) if self.check_mode: self.results['state'] = function_app.as_dict() elif self.results['changed']: try: new_function_app = self.web_client.web_apps.create_or_update( resource_group_name=self.resource_group, name=self.name, site_envelope=function_app ).result() self.results['state'] = new_function_app.as_dict() except CloudError as exc: self.fail('Error creating or updating web app: {}'.format(exc)) return self.results def update(self, source_function_app): """Update the Site object if there are any changes""" source_app_settings = self.web_client.web_apps.list_application_settings( resource_group_name=self.resource_group, name=self.name ) changed, target_app_settings = self.update_app_settings(source_app_settings.properties) source_function_app.site_config = SiteConfig( app_settings=target_app_settings, scm_type='LocalGit' ) return changed, source_function_app def update_app_settings(self, source_app_settings): """Update app settings""" target_app_settings = self.aggregated_app_settings() target_app_settings_dict = dict([(i.name, i.value) for i in target_app_settings]) return target_app_settings_dict != source_app_settings, target_app_settings def necessary_functionapp_settings(self): """Construct the necessary app settings required for an Azure Function App""" function_app_settings = [] for key in ['AzureWebJobsStorage', 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING', 'AzureWebJobsDashboard']: function_app_settings.append(NameValuePair(name=key, value=self.storage_connection_string)) function_app_settings.append(NameValuePair(name='FUNCTIONS_EXTENSION_VERSION', value='~1')) function_app_settings.append(NameValuePair(name='WEBSITE_NODE_DEFAULT_VERSION', value='6.5.0')) function_app_settings.append(NameValuePair(name='WEBSITE_CONTENTSHARE', value=self.name)) return function_app_settings def aggregated_app_settings(self): """Combine both system and user app settings""" function_app_settings = self.necessary_functionapp_settings() for app_setting_key in self.app_settings: function_app_settings.append(NameValuePair( name=app_setting_key, value=self.app_settings[app_setting_key] )) return function_app_settings @property def storage_connection_string(self): """Construct the storage account connection string""" return 'DefaultEndpointsProtocol=https;AccountName={};AccountKey={}'.format( self.storage_account, self.storage_key ) @property def storage_key(self): """Retrieve the storage account key""" return self.storage_client.storage_accounts.list_keys( resource_group_name=self.resource_group, account_name=self.storage_account ).keys[0].value def main(): """Main function execution""" AzureRMFunctionApp() if __name__ == '__main__': main()