Add support for Azure Functions (#28566)

* add template for az func

* (wip) add basic azure functions support

* add support to add app settings to azure function

* add support for updating based off of app settings

* add integration tests and refactor required param

* support check mode and add facts module

* add test for azure functions facts module

* add necessary checks and registrations for web client

* fix documentation

* change return type from complex to dict

* disable azure_rm_functionapp tests until stable

* remove dict comprehension for py2.6

* pepe has whitespace tumor
This commit is contained in:
Thomas Stringer 2017-08-29 21:54:58 -04:00 committed by Matt Davis
parent ef660f87b0
commit 8a6ae51f90
7 changed files with 610 additions and 1 deletions

1
.gitignore vendored
View file

@ -90,3 +90,4 @@ htmlcov/
.coverage .coverage
# ansible-test coverage results # ansible-test coverage results
test/units/.coverage.* test/units/.coverage.*
/test/integration/cloud-config-azure.yml

View file

@ -100,11 +100,13 @@ try:
from azure.mgmt.compute.version import VERSION as compute_client_version from azure.mgmt.compute.version import VERSION as compute_client_version
from azure.mgmt.resource.version import VERSION as resource_client_version from azure.mgmt.resource.version import VERSION as resource_client_version
from azure.mgmt.dns.version import VERSION as dns_client_version from azure.mgmt.dns.version import VERSION as dns_client_version
from azure.mgmt.web.version import VERSION as web_client_version
from azure.mgmt.network import NetworkManagementClient from azure.mgmt.network import NetworkManagementClient
from azure.mgmt.resource.resources import ResourceManagementClient from azure.mgmt.resource.resources import ResourceManagementClient
from azure.mgmt.storage import StorageManagementClient from azure.mgmt.storage import StorageManagementClient
from azure.mgmt.compute import ComputeManagementClient from azure.mgmt.compute import ComputeManagementClient
from azure.mgmt.dns import DnsManagementClient from azure.mgmt.dns import DnsManagementClient
from azure.mgmt.web import WebSiteManagementClient
from azure.mgmt.containerservice import ContainerServiceClient from azure.mgmt.containerservice import ContainerServiceClient
from azure.storage.cloudstorageaccount import CloudStorageAccount from azure.storage.cloudstorageaccount import CloudStorageAccount
except ImportError as exc: except ImportError as exc:
@ -135,7 +137,8 @@ AZURE_EXPECTED_VERSIONS = dict(
compute_client_version="1.0.0", compute_client_version="1.0.0",
network_client_version="1.0.0", network_client_version="1.0.0",
resource_client_version="1.1.0", resource_client_version="1.1.0",
dns_client_version="1.0.1" dns_client_version="1.0.1",
web_client_version="0.32.0"
) )
AZURE_MIN_RELEASE = '2.0.0' AZURE_MIN_RELEASE = '2.0.0'
@ -185,7 +188,9 @@ class AzureRMModuleBase(object):
self._resource_client = None self._resource_client = None
self._compute_client = None self._compute_client = None
self._dns_client = None self._dns_client = None
self._web_client = None
self._containerservice_client = None self._containerservice_client = None
self.check_mode = self.module.check_mode self.check_mode = self.module.check_mode
self.facts_module = facts_module self.facts_module = facts_module
# self.debug = self.module.params.get('debug') # self.debug = self.module.params.get('debug')
@ -727,6 +732,19 @@ class AzureRMModuleBase(object):
self._register('Microsoft.Dns') self._register('Microsoft.Dns')
return self._dns_client return self._dns_client
@property
def web_client(self):
self.log('Getting web client')
if not self._web_client:
self.check_client_version('web', web_client_version, AZURE_EXPECTED_VERSIONS['web_client_version'])
self._web_client = WebSiteManagementClient(
credentials=self.azure_credentials,
subscription_id=self.subscription_id,
base_url=self.base_url
)
self._register('Microsoft.Web')
return self._web_client
@property @property
def containerservice_client(self): def containerservice_client(self):
self.log('Getting container service client') self.log('Getting container service client')

View file

@ -0,0 +1,297 @@
#!/usr/bin/python
#
# Copyright (c) 2016 Thomas Stringer, <tomstr@microsoft.com>
#
# 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
name:
description:
- Name of the Azure Function App
required: true
state:
description:
- Assert the state of the Function App. Use 'present' to create or update a Function App and
'absent' to delete.
required: false
default: present
choices:
- absent
- present
extends_documentation_fragment:
- azure
author:
- "Thomas Stringer (@tstringer)"
'''
EXAMPLES = '''
- name: create function app
azure_rm_functionapp:
resource_group: ansible-rg
name: myfunctionapp
- name: create a function app with app settings
azure_rm_functionapp:
resource_group: ansible-rg
name: myfunctionapp
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', required=False),
storage_account=dict(
type='str',
required=False,
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.storage_account))
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()

View file

@ -0,0 +1,207 @@
#!/usr/bin/python
#
# Copyright (c) 2016 Thomas Stringer, <tomstr@microsoft.com>
# 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_facts
version_added: "2.4"
short_description: Get Azure Function App facts
description:
- Get facts for one Azure Function App or all Function Apps within a resource group
options:
name:
description:
- Only show results for a specific Function App
required: false
default: null
resource_group:
description:
- Limit results to a resource group. Required when filtering by name
required: false
default: null
aliases:
- resource_group_name
tags:
description:
- Limit results by providing a list of tags. Format tags as 'key' or 'key:value'.
required: false
default: null
extends_documentation_fragment:
- azure
author:
- "Thomas Stringer (@tstringer)"
'''
EXAMPLES = '''
- name: Get facts for one Function App
azure_rm_functionapp_facts:
resource_group: ansible-rg
name: myfunctionapp
- name: Get facts for all Function Apps in a resource group
azure_rm_functionapp_facts:
resource_group: ansible-rg
- name: Get facts for all Function Apps by tags
azure_rm_functionapp_facts:
tags:
- testing
'''
RETURN = '''
azure_functionapps:
description: List of Azure Function Apps dicts
returned: always
type: list
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
'''
try:
from msrestazure.azure_exceptions import CloudError
except:
# This is handled in azure_rm_common
pass
from ansible.module_utils.azure_rm_common import AzureRMModuleBase
class AzureRMFunctionAppFacts(AzureRMModuleBase):
def __init__(self):
self.module_arg_spec = dict(
name=dict(type='str'),
resource_group=dict(type='str', aliases=['resource_group_name']),
tags=dict(type='list'),
)
self.results = dict(
changed=False,
ansible_facts=dict(azure_functionapps=[])
)
self.name = None
self.resource_group = None
self.tags = None
super(AzureRMFunctionAppFacts, self).__init__(
self.module_arg_spec,
supports_tags=False,
facts_module=True
)
def exec_module(self, **kwargs):
for key in self.module_arg_spec:
setattr(self, key, kwargs[key])
if self.name and not self.resource_group:
self.fail("Parameter error: resource group required when filtering by name.")
if self.name:
self.results['ansible_facts']['azure_functionapps'] = self.get_functionapp()
elif self.resource_group:
self.results['ansible_facts']['azure_functionapps'] = self.list_resource_group()
else:
self.results['ansible_facts']['azure_functionapps'] = self.list_all()
return self.results
def get_functionapp(self):
self.log('Get properties for Function App {0}'.format(self.name))
function_app = None
result = []
try:
function_app = self.web_client.web_apps.get(
self.resource_group,
self.name
)
except CloudError:
pass
if function_app and self.has_tags(function_app.tags, self.tags):
result = function_app.as_dict()
return [result]
def list_resource_group(self):
self.log('List items')
try:
response = self.web_client.web_apps.list_by_resource_group(self.resource_group)
except Exception as exc:
self.fail("Error listing for resource group {0} - {1}".format(self.resource_group, str(exc)))
results = []
for item in response:
if self.has_tags(item.tags, self.tags):
results.append(item.as_dict())
return results
def list_all(self):
self.log('List all items')
try:
response = self.web_client.web_apps.list_by_resource_group(self.resource_group)
except Exception as exc:
self.fail("Error listing all items - {0}".format(str(exc)))
results = []
for item in response:
if self.has_tags(item.tags, self.tags):
results.append(item.as_dict())
return results
def main():
AzureRMFunctionAppFacts()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,2 @@
cloud/azure
destructive

View file

@ -0,0 +1,2 @@
dependencies:
- setup_azure

View file

@ -0,0 +1,82 @@
- name: create storage account for function apps
azure_rm_storageaccount:
resource_group: '{{ resource_group }}'
name: azfunccistor4
account_type: Standard_LRS
- name: create basic function app
azure_rm_functionapp:
resource_group: '{{ resource_group }}'
name: azfuncci
storage_account: azfunccistor4
register: output
- name: assert the function was created
assert:
that: output.changed
- name: list facts for function
azure_rm_functionapp_facts:
resource_group: '{{ resource_group }}'
name: azfuncci
- name: assert the facts were retrieved
assert:
that: '{{ ansible_facts.azure_functionapps|length == 1 }}'
- name: delete basic function app
azure_rm_functionapp:
resource_group: '{{ resource_group }}'
name: azfuncci
state: absent
register: output
- name: assert the function was deleted
assert:
that: output.changed
- name: create a function with app settings
azure_rm_functionapp:
resource_group: '{{ resource_group }}'
name: azfuncci
storage_account: azfunccistor4
app_settings:
hello: world
things: more stuff
register: output
- name: assert the function with app settings was created
assert:
that: output.changed
- name: change app settings
azure_rm_functionapp:
resource_group: '{{ resource_group }}'
name: azfuncci
storage_account: azfunccistor4
app_settings:
hello: world
things: more stuff
another: one
register: output
- name: assert the function was changed
assert:
that: output.changed
- name: delete the function app
azure_rm_functionapp:
resource_group: '{{ resource_group }}'
name: azfuncci
state: absent
register: output
- name: assert the function was deleted
assert:
that: output.changed
- name: delete storage account
azure_rm_storageaccount:
resource_group: '{{ resource_group }}'
name: azfunccistor4
state: absent