diff --git a/meta/runtime.yml b/meta/runtime.yml index 59c24bdfa..7c2d194c0 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -14,6 +14,8 @@ action_groups: - azure.azcollection.azure_rm_adserviceprincipal_info - azure.azcollection.azure_rm_aduser - azure.azcollection.azure_rm_aduser_info + - azure.azcollection.azure_rm_afdroute + - azure.azcollection.azure_rm_afdroute_info - azure.azcollection.azure_rm_aks - azure.azcollection.azure_rm_aks_info - azure.azcollection.azure_rm_aksagentpool diff --git a/plugins/modules/azure_rm_afdroute.py b/plugins/modules/azure_rm_afdroute.py new file mode 100644 index 000000000..341b096a0 --- /dev/null +++ b/plugins/modules/azure_rm_afdroute.py @@ -0,0 +1,749 @@ +#!/usr/bin/python +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Python SDK Reference: https://learn.microsoft.com/en-us/python/api/azure-mgmt-cdn/azure.mgmt.cdn.operations.routesoperations?view=azure-python +# +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: azure_rm_afdroute +version_added: "2.4.0" +short_description: Manage an Azure Front Door Route +description: + - Create, update and delete an Azure Front Door Route to be used by a Front Door Service Profile created using azure_rm_cdnprofile. + +options: + content_types_to_compress: + description: + - The caching configuration/compression settings for this route. + - List of content types (str) on which compression applies. The value should be a valid MIME type. + - Required together (is_compression_enabled, content_types_to_compress, query_string_caching_behavior, query_parameters) + type: list + elements: str + custom_domains: + description: + - Domain names referenced by this endpoint. ID will be looked up based on the name. + type: list + elements: str + disable_cache_configuration: + description: + - To disable cache configuration, set this to true and do not include cache_configuration. + - This will override any cache_configuration settings you include. + type: bool + default: false + enabled_state: + description: + - Whether to enable use of this rule. Permitted values are 'Enabled' or 'Disabled'. + type: str + choices: + - Enabled + - Disabled + endpoint_name: + description: + - Name of the endpoint under the profile which is unique globally. + required: true + type: str + forwarding_protocol: + description: + - Protocol this rule will use when forwarding traffic to backends. + type: str + choices: + - HttpOnly + - HttpsOnly + - MatchRequest + https_redirect: + description: + - Whether to automatically redirect HTTP traffic to HTTPS traffic. + type: str + default: Disabled + choices: + - Enabled + - Disabled + is_compression_enabled: + description: + - The caching configuration/compression settings for this route. + - Indicates whether content compression is enabled on AzureFrontDoor. + - If compression is enabled, content will be served as compressed if user requests for a compressed version. + - Content won't be compressed on AzureFrontDoor when requested content is smaller than 1 byte or larger than 1 MB. + - Required together (is_compression_enabled, content_types_to_compress, query_string_caching_behavior, query_parameters) + type: bool + link_to_default_domain: + description: + - whether this route will be linked to the default endpoint domain. + type: str + default: Disabled + choices: + - Enabled + - Disabled + name: + description: + - Name of the routing rule. + required: true + type: str + origin_group: + description: + - A reference to the origin group. + type: str + origin_path: + description: + - A directory path on the origin that AzureFrontDoor can use to retrieve content from, e.g. contoso.cloudapp.net/originpath. + type: str + patterns_to_match: + description: + - The route patterns of the rule. + type: list + elements: str + profile_name: + description: + - Name of the Azure Front Door Standard or Azure Front Door Premium profile which is unique within the resource group. + required: true + type: str + query_parameters: + description: + - The caching configuration for this route. + - query parameters to include or exclude (comma separated). + - Required together (is_compression_enabled, content_types_to_compress, query_string_caching_behavior, query_parameters) + type: str + query_string_caching_behavior: + description: + - The caching configuration for this route. + - Defines how Frontdoor caches requests that include query strings. + - You can ignore any query strings when caching, ignore specific query strings, + - cache every request with a unique URL, or cache specific query strings. + - Required together (is_compression_enabled, content_types_to_compress, query_string_caching_behavior, query_parameters) + type: str + choices: + - IgnoreQueryString + - IgnoreSpecifiedQueryStrings + - IncludeSpecifiedQueryStrings + - UseQueryString + resource_group: + description: + - Name of the Resource group within the Azure subscription. + required: true + type: str + rule_sets: + description: + - List of rule set names referenced by this endpoint. + type: list + elements: str + state: + description: + - Assert the state of the Route. Use C(present) to create or update a CDN profile and C(absent) to delete it. + default: present + type: str + choices: + - absent + - present + supported_protocols: + description: + - List of supported protocols for this route. + type: list + elements: str + default: ['Http', 'Https'] + choices: + - Http + - Https + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - Jarret Tooley (@jartoo) +''' + +EXAMPLES = ''' +- name: Create an AFD Route + azure_rm_afdroute: + name: myRoute + endpoint_name: myEndpoint + origin_group: myOriginGroup + profile_name: myProfile + resource_group_name: myResourceGroup + state: present + route: + enabled_state: Disabled + forwarding_protocol: HttpsOnly + https_redirect: Enabled + patterns_to_match: + - "/*" + rule_sets: + - Security + supported_protocols: + - Https + - Http + link_to_default_domain: Enabled + +- name: Delete an AFD Origin + azure_rm_afdroute: + name: myRoute + endpoint_name: myEndpoint + origin_group: myOriginGroup + profile_name: myProfile + resource_group_name: myResourceGroup + state: absent +''' +RETURN = ''' +id: + description: + - ID of the Route. + returned: always + type: str + sample: "id: '/subscriptions/xxxxxx-xxxx-xxxx-xxxx-xxxxxxxx/resourcegroups/myRG/providers/Microsoft.Cdn/profiles/myProf/afdendpoints/myEP/routes/myRoute'" +''' +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase +# from azure.core.serialization import NULL as AzureCoreNull + +try: + from azure.mgmt.cdn.models import Route, RouteUpdateParameters, CompressionSettings, \ + ResourceReference, AfdRouteCacheConfiguration + from azure.mgmt.cdn import CdnManagementClient + +except ImportError as ec: + # This is handled in azure_rm_common + pass + + +class AzureRMRoute(AzureRMModuleBase): + ''' Main Class ''' + def __init__(self): + self.module_arg_spec = dict( + content_types_to_compress=dict( + type='list', + elements='str' + ), + custom_domains=dict( + type='list', + elements='str' + ), + disable_cache_configuration=dict( + type='bool', + default=False + ), + enabled_state=dict( + type='str', + choices=['Enabled', 'Disabled'] + ), + endpoint_name=dict( + type='str', + required=True + ), + forwarding_protocol=dict( + type='str', + choices=['HttpOnly', 'HttpsOnly', 'MatchRequest'] + ), + https_redirect=dict( + type='str', + choices=['Enabled', 'Disabled'], + default='Disabled' + ), + is_compression_enabled=dict( + type='bool' + ), + link_to_default_domain=dict( + type='str', + choices=['Enabled', 'Disabled'], + default='Disabled' + ), + name=dict( + type='str', + required=True + ), + origin_group=dict( + type='str' + ), + origin_path=dict( + type='str' + ), + patterns_to_match=dict( + type='list', + elements='str' + ), + profile_name=dict( + type='str', + required=True + ), + query_string_caching_behavior=dict( + type='str', + choices=['IgnoreQueryString', 'IgnoreSpecifiedQueryStrings', 'IncludeSpecifiedQueryStrings', 'UseQueryString'] + ), + query_parameters=dict( + type='str' + ), + resource_group=dict( + type='str', + required=True + ), + rule_sets=dict( + type='list', + elements='str' + ), + state=dict( + type='str', + default='present', + choices=['present', 'absent'] + ), + supported_protocols=dict( + type='list', + elements='str', + choices=['Http', 'Https'], + default=['Http', 'Https'] + ) + ) + + self.content_types_to_compress = None + self.custom_domains = None + self.disable_cache_configuration = None + self.enabled_state = None + self.forwarding_protocol = None + self.https_redirect = None + self.is_compression_enabled = None + self.link_to_default_domain = None + self.origin_path = None + self.origin_group = None + self.patterns_to_match = None + self.query_parameters = None + self.query_string_caching_behavior = None + self.rule_sets = [] + self.supported_protocols = None + + self.origin_group_id = None + + self.name = None + self.endpoint_name = None + self.profile_name = None + self.resource_group = None + self.state = None + + self.rule_set_ids = [] + self.custom_domain_ids = [] + + self.route_client = None + + required_together = [['is_compression_enabled', 'content_types_to_compress', 'query_string_caching_behavior', 'query_parameters']] + + self.results = dict(changed=False) + + super(AzureRMRoute, self).__init__( + derived_arg_spec=self.module_arg_spec, + supports_check_mode=True, + supports_tags=False, + required_together=required_together + ) + + def exec_module(self, **kwargs): + """Main module execution method""" + + for key in list(self.module_arg_spec.keys()): + setattr(self, key, kwargs[key]) + + self.route_client = self.get_route_client() + + to_be_updated = False + + # Ignore query_parameters when query_string_caching_behavior is "IgnoreQueryString" or "UseQueryString" + if self.query_string_caching_behavior in ["IgnoreQueryString", "UseQueryString"]: + self.query_parameters = None + self.log("Ignoring query_parameters when query_string_caching_behavior is IgnoreQueryString or UseQueryString") + + # Get the existing resource + response = self.get_route() + + if self.state == 'present': + # Get the Origin Group ID + self.origin_group_id = self.get_origin_group_id() + if self.origin_group_id is False: + self.fail("Could not obtain Origin Group ID from {0}, please create it first.".format(self.origin_group)) + + # Get a list of all the Custom Domain IDs + if isinstance(self.custom_domains, list): + if len(self.custom_domains) > 0: + for custom_domain in self.custom_domains: + cd_id = self.get_custom_domain_id(custom_domain) + if cd_id: + ref = ResourceReference( + id=cd_id) + self.custom_domain_ids.append(ref) + + # Populate the rule_set_ids + convert_rules = self.get_rule_set_ids() + if not convert_rules: + self.fail("Failed to convert the Rule Set names to IDs") + + if not response: + self.log("Need to create the Route") + + if not self.check_mode: + new_results = self.create_route() + self.results['id'] = new_results['id'] + self.results['changed'] = True + + else: + self.log('Results : {0}'.format(response)) + + to_be_updated = self.update_needed(response) + + self.results['id'] = response['id'] + if to_be_updated: + self.log("Need to update the Route") + + if not self.check_mode: + new_results = self.update_route() + self.results['id'] = new_results['id'] + + self.results['changed'] = True + + elif self.state == 'absent': + if not response: + self.log("Route {0} does not exist.".format(self.name)) + self.results['id'] = None + self.results['changed'] = False + else: + self.log("Need to delete the Route") + self.results['changed'] = True + + if not self.check_mode: + self.delete_route() + self.results['id'] = response['id'] + return self.results + + def update_needed(self, response): + ''' + Check if the resource needs to be updated + + return: bool + ''' + to_be_updated = False + cache_configuration = response['cache_configuration'] + if cache_configuration and self.disable_cache_configuration: + to_be_updated = True + if cache_configuration: + if cache_configuration.query_parameters != self.query_parameters and self.query_parameters: + to_be_updated = True + if cache_configuration.query_string_caching_behavior != self.query_string_caching_behavior and self.query_string_caching_behavior: + to_be_updated = True + if cache_configuration.compression_settings.is_compression_enabled != self.is_compression_enabled and self.is_compression_enabled: + to_be_updated = True + if cache_configuration.compression_settings.content_types_to_compress != self.content_types_to_compress and self.content_types_to_compress: + to_be_updated = True + else: + if self.is_compression_enabled or self.query_parameters or self.query_string_caching_behavior or self.content_types_to_compress: + to_be_updated = True + # Test for custom_domain equality + equal = True + if len(response['custom_domains']) != len(self.custom_domain_ids): + equal = False + for x in response['custom_domains']: + found = False + for y in self.custom_domain_ids: + if x.id == y.id: + found = True + continue + if not found: + equal = False + if not equal: + to_be_updated = True + + if response['enabled_state'] != self.enabled_state and self.enabled_state: + to_be_updated = True + if response['forwarding_protocol'] != self.forwarding_protocol and self.forwarding_protocol: + to_be_updated = True + if response['https_redirect'] != self.https_redirect and self.https_redirect: + to_be_updated = True + if response['link_to_default_domain'] != self.link_to_default_domain and self.link_to_default_domain: + to_be_updated = True + if response['origin_group_id'] != self.origin_group_id and self.origin_group_id: + to_be_updated = True + if response['origin_path'] != self.origin_path and self.origin_path: + to_be_updated = True + if response['patterns_to_match'] != self.patterns_to_match and self.patterns_to_match: + to_be_updated = True + if response["rule_sets"] != self.rule_set_ids and self.rule_set_ids: + to_be_updated = True + if response['supported_protocols'] != self.supported_protocols and self.supported_protocols: + to_be_updated = True + + return to_be_updated + + def create_route(self): + ''' + Creates a Azure Route. + + :return: deserialized Azure Route instance state dictionary + ''' + self.log("Creating the Azure Route instance {0}".format(self.name)) + cache_configuration = None + compression_settings = None + + if self.disable_cache_configuration: + # cache_configuration = AzureCoreNull # Reported as issue to azure-mgmt-cdn: https://github.com/Azure/azure-sdk-for-python/issues/35801 + cache_configuration = None + else: + if not self.is_compression_enabled and not self.content_types_to_compress: + compression_settings = None + else: + compression_settings = CompressionSettings( + content_types_to_compress=self.content_types_to_compress, + is_compression_enabled=self.is_compression_enabled + ) + if not self.query_string_caching_behavior and not self.query_parameters and not compression_settings: + cache_configuration = None + else: + cache_configuration = AfdRouteCacheConfiguration( + query_string_caching_behavior=self.query_string_caching_behavior, + query_parameters=self.query_parameters, + compression_settings=compression_settings + ) + + origin_group = ResourceReference( + id=self.origin_group_id + ) + + parameters = Route( + cache_configuration=cache_configuration, + custom_domains=self.custom_domain_ids, + enabled_state=self.enabled_state, + forwarding_protocol=self.forwarding_protocol, + https_redirect=self.https_redirect, + link_to_default_domain=self.link_to_default_domain, + origin_group=origin_group, + origin_path=self.origin_path, + patterns_to_match=self.patterns_to_match, + rule_sets=self.rule_set_ids, + supported_protocols=self.supported_protocols + ) + + try: + poller = self.route_client.routes.begin_create( + resource_group_name=self.resource_group, + profile_name=self.profile_name, + endpoint_name=self.endpoint_name, + route_name=self.name, + route=parameters + ) + response = self.get_poller_result(poller) + return route_to_dict(response) + except Exception as exc: + self.log('Error attempting to create Azure Route instance.') + self.fail("Error Creating Azure Route instance: {0}".format(str(exc))) + + def update_route(self): + ''' + Updates an Azure Route. + + :return: deserialized Azure Route instance state dictionary + ''' + self.log("Updating the Azure Route instance {0}".format(self.name)) + + cache_configuration = None + compression_settings = None + + if self.disable_cache_configuration: + # cache_configuration = AzureCoreNull # Reported as issue to azure-mgmt-cdn: https://github.com/Azure/azure-sdk-for-python/issues/35801 + cache_configuration = None + else: + if not self.is_compression_enabled and not self.content_types_to_compress: + compression_settings = None + else: + compression_settings = CompressionSettings( + content_types_to_compress=self.content_types_to_compress, + is_compression_enabled=self.is_compression_enabled + ) + if not self.query_string_caching_behavior and not self.query_parameters and not compression_settings: + cache_configuration = None + else: + cache_configuration = AfdRouteCacheConfiguration( + query_string_caching_behavior=self.query_string_caching_behavior, + query_parameters=self.query_parameters, + compression_settings=compression_settings + ) + + origin_group = ResourceReference( + id=self.origin_group_id + ) + + parameters = RouteUpdateParameters( + cache_configuration=cache_configuration, + custom_domains=self.custom_domain_ids, + enabled_state=self.enabled_state, + forwarding_protocol=self.forwarding_protocol, + https_redirect=self.https_redirect, + link_to_default_domain=self.link_to_default_domain, + origin_group=origin_group, + origin_path=self.origin_path, + patterns_to_match=self.patterns_to_match, + rule_sets=self.rule_set_ids, + supported_protocols=self.supported_protocols + ) + + try: + poller = self.route_client.routes.begin_update( + resource_group_name=self.resource_group, + profile_name=self.profile_name, + endpoint_name=self.endpoint_name, + route_name=self.name, + route_update_properties=parameters + ) + response = self.get_poller_result(poller) + return route_to_dict(response) + except Exception as exc: + self.log('Error attempting to update Azure Route instance.') + self.fail("Error updating Azure Route instance: {0}".format(str(exc))) + + def delete_route(self): + ''' + Deletes the specified Azure Route in the specified subscription and resource group. + + :return: True + ''' + self.log("Deleting the Route {0}".format(self.name)) + try: + poller = self.route_client.routes.begin_delete( + resource_group_name=self.resource_group, + profile_name=self.profile_name, + endpoint_name=self.endpoint_name, + route_name=self.name + ) + self.get_poller_result(poller) + return True + except Exception as exc: + self.log('Error attempting to delete the Route.') + self.fail("Error deleting the Route: {0}".format(str(exc))) + return False + + def get_route(self): + ''' + Gets the properties of the specified Route. + + :return: deserialized Route state dictionary + ''' + self.log( + "Checking if the Route {0} is present".format(self.name)) + try: + response = self.route_client.routes.get( + resource_group_name=self.resource_group, + profile_name=self.profile_name, + endpoint_name=self.endpoint_name, + route_name=self.name, + ) + self.log("Response : {0}".format(response)) + self.log("Route : {0} found".format(response.name)) + return route_to_dict(response) + except Exception as err: + self.log('Did not find the Route.' + err.args[0]) + return False + + def get_origin_group_id(self): + ''' + Gets the ID of the specified Origin Group. + + :return: ID for the Origin Group. + ''' + self.log( + "Obtaining ID for Origin Group {0}".format(self.origin_group)) + try: + response = self.route_client.afd_origin_groups.get( + resource_group_name=self.resource_group, + profile_name=self.profile_name, + origin_group_name=self.origin_group + ) + self.log("Response : {0}".format(response)) + self.log("Origin Group ID found : {0} found".format(response.id)) + return response.id + except Exception as err: + self.log('Did not find the Origin Group.' + err.args[0]) + return False + + def get_custom_domain_id(self, custom_domain): + ''' + Gets the ID of the specified Custom Domain. + + :return: ID for the Custom Domain. + ''' + self.log("Obtaining ID for Custom Domain {0}".format(self.origin_group)) + try: + response = self.route_client.afd_custom_domains.get( + resource_group_name=self.resource_group, + profile_name=self.profile_name, + custom_domain_name=custom_domain + ) + self.log("Response : {0}".format(response)) + self.log("Custom Domain found : {0} found".format(response.id)) + return response.id + except Exception as err: + self.log('Did not find the Custom Domain.' + err.args[0]) + return False + + def get_rule_set_ids(self): + ''' + Gets the IDs of the specified Rule Sets. + + :return: Boolean if Rule Sets were found and translated. + ''' + if self.rule_sets is None or len(self.rule_sets) == 0: + return True + + self.log("Obtaining IDs for Rule Sets") + + try: + for rule_name in self.rule_sets: + response = self.route_client.rule_sets.get( + resource_group_name=self.resource_group, + profile_name=self.profile_name, + rule_set_name=rule_name, + ) + self.log("Response : {0}".format(response)) + self.log("Rule Set ID found : {0} found".format(response.id)) + self.rule_set_ids.append(ResourceReference(id=response.id)) + return True + except Exception as err: + self.log('Error getting the Rule Set IDs.' + err.args[0]) + return False + + def get_route_client(self): + ''' Obtain the client object to use ''' + if not self.route_client: + self.route_client = self.get_mgmt_svc_client( + CdnManagementClient, + base_url=self._cloud_environment.endpoints.resource_manager, + api_version='2023-05-01' + ) + return self.route_client + + +def route_to_dict(route): + ''' + Convert the object to dictionary + ''' + return dict( + custom_domains=route.custom_domains, + cache_configuration=route.cache_configuration, + deployment_status=route.deployment_status, + enabled_state=route.enabled_state, + forwarding_protocol=route.forwarding_protocol, + https_redirect=route.https_redirect, + id=route.id, + link_to_default_domain=route.link_to_default_domain, + name=route.name, + origin_group_id=route.origin_group.id, + origin_path=route.origin_path, + patterns_to_match=route.patterns_to_match, + provisioning_state=route.provisioning_state, + rule_sets=route.rule_sets, + supported_protocols=route.supported_protocols, + type=route.type + ) + + +def main(): + """Main execution""" + AzureRMRoute() + # x = CdnManagementClient() + # x.routes.begin_update() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/azure_rm_afdroute_info.py b/plugins/modules/azure_rm_afdroute_info.py new file mode 100644 index 000000000..dd4611ac1 --- /dev/null +++ b/plugins/modules/azure_rm_afdroute_info.py @@ -0,0 +1,336 @@ +#!/usr/bin/python +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Python SDK Reference: https://learn.microsoft.com/en-us/python/api/azure-mgmt-cdn/azure.mgmt.cdn.operations.routesoperations?view=azure-python +# + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: azure_rm_afdroute_info +version_added: "2.4.0" +short_description: Get Azure Front Door Route facts to be used with Standard or Premium Frontdoor Service +description: + - Get facts for a specific Azure Front Door (AFD) Route or all AFD Routes. + - This differs from the Front Door classic service and only is intended to be used by the Standard or Premium service offering. + +options: + endpoint_name: + description: + - Name of the endpoint under the profile which is unique globally. + required: true + type: str + name: + description: + - Name of the route. + type: str + profile_name: + description: + - Name of the Azure Front Door Standard or Azure Front Door Premium profile which is unique within the resource group + required: true + type: str + resource_group: + description: + - Name of the Resource group within the Azure subscription. + required: true + type: str + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - Jarret Tooley (@jartoo) +''' + +EXAMPLES = ''' +- name: Get facts for all Routes in the AFD Profile + azure_rm_afdroute_info: + endpoint_name: myEndpoint + profile_name: myProfile + resource_group: myResourceGroup + +- name: Get facts of specific AFD Route + azure_rm_afdroute_info: + name: myRoute1 + endpoint_name: myEndpoint + profile_name: myProfile + resource_group: myResourceGroup +''' + +RETURN = ''' +afdroutes: + description: List of AFD Routes. + returned: always + type: complex + contains: + content_types_to_compress: + description: + - List of content types (str) on which compression applies. The value should be a valid MIME type. + type: list + custom_domains: + description: + - Domain id's referenced by this endpoint. + type: list + deployment_status: + description: + - Current state of the resource. + type: str + sample: NotStarted + enabled_state: + description: + - Whether to enable use of this rule. + type: str + endpoint_name: + description: + - Name of the endpoint. + type: str + forwarding_protocol: + description: + - Protocol this rule will use when forwarding traffic to backends. + type: str + https_redirect: + description: + - Whether to automatically redirect HTTP traffic to HTTPS traffic. + - Note that this is a easy way to set up this rule and it will be the first rule that gets executed. + type: str + id: + description: + - ID of the AFD Route. + type: str + sample: "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxx/resourcegroups/myRG/providers/Microsoft.Cdn/profiles/myProf/routegroups/myRtGrp/routes/myRt" + is_compression_enabled: + description: + - Indicates whether content compression is enabled on AzureFrontDoor. If compression is enabled, + - content will be served as compressed if user requests for a compressed version. Content won't be compressed + - on AzureFrontDoor when requested content is smaller than 1 byte or larger than 1 MB. + type: bool + link_to_default_domain: + description: + - Whether this route will be linked to the default endpoint domain. + type: str + name: + description: + - Name of the AFD Route. + type: str + origin_group_id: + description: + - The origin group id. + type: str + sample: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx/resourcegroups/myRG/providers/Microsoft.Cdn/profiles/myProf/origingroups/myOG + origin_path: + description: + - A directory path on the origin that AzureFrontDoor can use to retrieve content from, e.g. contoso.cloudapp.net/originpath. + type: str + patterns_to_match: + description: + - The route patterns of the rule. + type: list + profile_name: + description: + - Name of the AFD Profile where the Route is. + type: str + provisioning_state: + description: + - Provisioning status of the AFD Route. + type: str + sample: Succeeded + query_parameters: + description: + - query parameters to include or exclude (comma separated). + type: str + query_string_caching_behavior: + description: + - Defines how Frontdoor caches requests that include query strings. You can ignore any query strings when caching, + - ignore specific query strings, cache every request with a unique URL, or cache specific query strings. + type: str + resource_group_name: + description: + - Name of a resource group where the AFD Route exists. + type: str + rule_sets: + description: + - List of rule set id referenced by this endpoint. + type: list + supported_protocols: + description: + - List of supported protocols for this route. + type: list + type: + description: + - Resource type. + type: str + sample: Microsoft.Cdn/profiles/afdendpoints/routes +''' + +import re +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + +try: + from azure.mgmt.cdn import CdnManagementClient +except ImportError: + # handled in azure_rm_common + pass + + +AZURE_OBJECT_CLASS = 'AFDRoute' + + +class AzureRMAFDRouteInfo(AzureRMModuleBase): + """Utility class to get Azure AFD Route facts""" + + def __init__(self): + + self.module_args = dict( + name=dict(type='str'), + endpoint_name=dict( + type='str', + required=True + ), + profile_name=dict( + type='str', + required=True + ), + resource_group=dict( + type='str', + required=True + ) + ) + + self.results = dict( + changed=False, + afdroutes=[] + ) + + self.name = None + self.endpoint_name = None + self.resource_group = None + self.profile_name = None + + super(AzureRMAFDRouteInfo, self).__init__( + supports_check_mode=True, + derived_arg_spec=self.module_args, + supports_tags=False, + facts_module=True + ) + + def exec_module(self, **kwargs): + + for key in self.module_args: + setattr(self, key, kwargs[key]) + + self.route_client = self.get_mgmt_svc_client( + CdnManagementClient, + base_url=self._cloud_environment.endpoints.resource_manager, + api_version='2023-05-01' + ) + + if self.name: + self.results['afdroutes'] = self.get_item() + else: + self.results['afdroutes'] = self.list_by_endpoint() + + return self.results + + def get_item(self): + """Get a single Azure AFD Route""" + + self.log('Get properties for {0}'.format(self.name)) + + item = None + result = [] + + try: + item = self.route_client.routes.get( + resource_group_name=self.resource_group, + profile_name=self.profile_name, + endpoint_name=self.endpoint_name, + route_name=self.name + ) + except Exception as exc: + self.log("Failed to find an existing resource. {0}".format(str(exc))) + + if item: + result = [self.serialize_afdroute(item)] + + return result + + def list_by_endpoint(self): + """Get all Azure AFD Routes within an AFD profile""" + + self.log('List all AFD Routes within an AFD profile') + + try: + response = self.route_client.routes.list_by_endpoint( + resource_group_name=self.resource_group, + profile_name=self.profile_name, + endpoint_name=self.endpoint_name + ) + except Exception as exc: + self.fail('Failed to list all items - {0}'.format(str(exc))) + + results = [] + for item in response: + results.append(self.serialize_afdroute(item)) + + return results + + def serialize_afdroute(self, afdroute): + ''' + Convert a AFD Route object to dict. + :param afdroute: AFD Route object + :return: dict + ''' + result = self.serialize_obj(afdroute, AZURE_OBJECT_CLASS) + + new_result = {} + new_result['content_types_to_compress'] = None + new_result['is_compression_enabled'] = None + new_result['query_parameters'] = None + new_result['query_string_caching_behavior'] = None + if afdroute.cache_configuration: + new_result['content_types_to_compress'] = \ + afdroute.cache_configuration.compression_settings.content_types_to_compress + new_result['is_compression_enabled'] = \ + afdroute.cache_configuration.compression_settings.is_compression_enabled + new_result['query_parameters'] = afdroute.cache_configuration.query_parameters + new_result['query_string_caching_behavior'] = afdroute.cache_configuration.query_string_caching_behavior + new_result['custom_domains'] = [] + for domain in afdroute.custom_domains: + dom = { + "id": domain.id, + "is_active": domain.is_active + } + new_result['custom_domains'].append(dom) + new_result['deployment_status'] = afdroute.deployment_status + new_result['enabled_state'] = afdroute.enabled_state + new_result['endpoint_name'] = self.endpoint_name + new_result['forwarding_protocol'] = afdroute.forwarding_protocol + new_result['https_redirect'] = afdroute.https_redirect + new_result['id'] = afdroute.id + new_result['link_to_default_domain'] = afdroute.link_to_default_domain + new_result['name'] = afdroute.name + new_result['origin_group_id'] = afdroute.origin_group.id + new_result['origin_path'] = afdroute.origin_path + new_result['patterns_to_match'] = afdroute.patterns_to_match + new_result['provisioning_state'] = afdroute.provisioning_state + new_result['rule_sets'] = [] + for rule_set in afdroute.rule_sets: + new_result['rule_sets'].append(rule_set.id) + new_result['profile_name'] = re.sub('\\/.*', '', re.sub('.*profiles\\/', '', result['id'])) + new_result['resource_group_name'] = re.sub('\\/.*', '', re.sub('.*resourcegroups\\/', '', result['id'])) + new_result['supported_protocols'] = afdroute.supported_protocols + new_result['type'] = afdroute.type + return new_result + + +def main(): + """Main module execution code path""" + AzureRMAFDRouteInfo() + + +if __name__ == '__main__': + main() diff --git a/pr-pipelines.yml b/pr-pipelines.yml index 23c9b562e..210adf9cf 100644 --- a/pr-pipelines.yml +++ b/pr-pipelines.yml @@ -35,6 +35,7 @@ parameters: - "azure_rm_acs" - "azure_rm_adgroup" - "azure_rm_aduser" + - "azure_rm_afdroute" - "azure_rm_aks" - "azure_rm_aksagentpool" - "azure_rm_apimanagement" diff --git a/tests/integration/targets/azure_rm_afdroute/aliases b/tests/integration/targets/azure_rm_afdroute/aliases new file mode 100644 index 000000000..3b050cbc1 --- /dev/null +++ b/tests/integration/targets/azure_rm_afdroute/aliases @@ -0,0 +1,3 @@ +cloud/azure +destructive +shippable/azure/group6 diff --git a/tests/integration/targets/azure_rm_afdroute/meta/main.yml b/tests/integration/targets/azure_rm_afdroute/meta/main.yml new file mode 100644 index 000000000..95e1952f9 --- /dev/null +++ b/tests/integration/targets/azure_rm_afdroute/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_azure diff --git a/tests/integration/targets/azure_rm_afdroute/tasks/main.yml b/tests/integration/targets/azure_rm_afdroute/tasks/main.yml new file mode 100644 index 000000000..55a2b3439 --- /dev/null +++ b/tests/integration/targets/azure_rm_afdroute/tasks/main.yml @@ -0,0 +1,258 @@ +- name: Prepare random name for Profile & EndPoint & Origin Group & Origin + ansible.builtin.set_fact: + profile: "prof-{{ resource_group | hash('md5') | truncate(4, True, '') }}{{ 100000 | random }}" + endpoint: "endpoint-{{ resource_group | hash('md5') | truncate(4, True, '') }}{{ 100000 | random }}" + origin_group: "origin-group-{{ resource_group | hash('md5') | truncate(4, True, '') }}{{ 100000 | random }}" + ruleset: "ruleset{{ resource_group | hash('md5') | truncate(4, True, '') }}{{ 100000 | random }}" + origin: "origin-{{ resource_group | hash('md5') | truncate(4, True, '') }}{{ 100000 | random }}" + route: "route-{{ resource_group | hash('md5') | truncate(4, True, '') }}{{ 100000 | random }}" + +- name: Create Standard Frontdoor Profile + azure_rm_cdnprofile: + name: "{{ profile }}" + location: "Global" + resource_group: "{{ resource_group }}" + sku: "standard_azurefrontdoor" + state: "present" + +- name: Create EndPoint + azure_rm_afdendpoint: + name: "{{ endpoint }}" + profile_name: "{{ profile }}" + resource_group: "{{ resource_group }}" + enabled_state: "Enabled" + state: "present" + +- name: Create Origin Group + azure_rm_afdorigingroup: + name: "{{ origin_group }}" + profile_name: "{{ profile }}" + resource_group: "{{ resource_group }}" + state: "present" + health_probe_settings: + probe_interval_in_seconds: 100 + probe_path: "/" + probe_protocol: "Https" + probe_request_type: "HEAD" + load_balancing_settings: + additional_latency_in_milliseconds: 50 + sample_size: 4 + successful_samples_required: 3 + register: output + +- name: Create Origin + azure_rm_afdorigin: + name: "{{ origin }}" + origin_group_name: "{{ origin_group }}" + profile_name: "{{ profile }}" + resource_group: "{{ resource_group }}" + state: "present" + host_name: "10.0.0.1" + origin_host_header: "10.0.0.1" + http_port: 80 + https_port: 443 + priority: 1 + weight: 123 + register: output + +- name: Create Ruleset + azure_rm_afdruleset: + name: "{{ ruleset }}" + profile_name: "{{ profile }}" + resource_group: "{{ resource_group }}" + state: "present" + +- name: Create Route + azure_rm_afdroute: + name: "{{ route }}" + endpoint_name: "{{ endpoint }}" + profile_name: "{{ profile }}" + resource_group: "{{ resource_group }}" + enabled_state: "Enabled" + forwarding_protocol: "HttpsOnly" + https_redirect: "Disabled" + link_to_default_domain: "Enabled" + origin_group: "{{ origin_group }}" + patterns_to_match: + - "/auto/*" + rule_sets: + - "{{ ruleset }}" + supported_protocols: + - "Https" + state: "present" + register: output + +- name: Assert the resource was created + ansible.builtin.assert: + that: + - output.changed + - output.id + +- name: Update Route (idempotent test) + azure_rm_afdroute: + name: "{{ route }}" + endpoint_name: "{{ endpoint }}" + profile_name: "{{ profile }}" + resource_group: "{{ resource_group }}" + enabled_state: "Enabled" + forwarding_protocol: "HttpsOnly" + https_redirect: "Disabled" + link_to_default_domain: "Enabled" + origin_group: "{{ origin_group }}" + patterns_to_match: + - "/auto/*" + rule_sets: + - "{{ ruleset }}" + supported_protocols: + - "Https" + state: "present" + register: output + +- name: Assert the resource was not changed + ansible.builtin.assert: + that: + - not output.changed + - output.id + +- name: Load Route info + azure_rm_afdroute_info: + name: "{{ route }}" + endpoint_name: "{{ endpoint }}" + profile_name: "{{ profile }}" + resource_group: "{{ resource_group }}" + register: output + +- name: Assert the resource has the proper values set + ansible.builtin.assert: + that: + - output.afdroutes | length == 1 + - output.afdroutes[0].enabled_state == 'Enabled' + - output.afdroutes[0].id + - output.afdroutes[0].rule_sets | length == 1 + - not output.changed + +- name: Update Route (with different values) + azure_rm_afdroute: + name: "{{ route }}" + endpoint_name: "{{ endpoint }}" + profile_name: "{{ profile }}" + resource_group: "{{ resource_group }}" + enabled_state: "Disabled" + forwarding_protocol: "HttpsOnly" + https_redirect: "Disabled" + link_to_default_domain: "Enabled" + origin_group: "{{ origin_group }}" + patterns_to_match: + - "/auto/*" + - "/test/*" + rule_sets: + - "{{ ruleset }}" + supported_protocols: + - "Https" + - "Http" + state: "present" + register: output + +- name: Assert the resource was changed + ansible.builtin.assert: + that: + - output.changed + - output.id + +- name: Update Route (idempotent test) + azure_rm_afdroute: + name: "{{ route }}" + endpoint_name: "{{ endpoint }}" + profile_name: "{{ profile }}" + resource_group: "{{ resource_group }}" + enabled_state: "Disabled" + forwarding_protocol: "HttpsOnly" + https_redirect: "Disabled" + link_to_default_domain: "Enabled" + origin_group: "{{ origin_group }}" + patterns_to_match: + - "/auto/*" + - "/test/*" + rule_sets: + - "{{ ruleset }}" + supported_protocols: + - "Https" + - "Http" + state: "present" + register: output + +- name: Assert the resource was not changed + ansible.builtin.assert: + that: + - not output.changed + - output.id + +- name: Delete Route + azure_rm_afdroute: + name: "{{ route }}" + endpoint_name: "{{ endpoint }}" + profile_name: "{{ profile }}" + resource_group: "{{ resource_group }}" + origin_group: "{{ origin_group }}" + state: "absent" + register: output + +- name: Assert the resource was changed + ansible.builtin.assert: + that: + - output.changed + - output.id + +- name: Delete Route (idempotent test) + azure_rm_afdroute: + name: "{{ route }}" + endpoint_name: "{{ endpoint }}" + profile_name: "{{ profile }}" + origin_group: "{{ origin_group }}" + resource_group: "{{ resource_group }}" + state: "absent" + register: output + +- name: Assert the resource was not changed + ansible.builtin.assert: + that: + - not output.changed + +- name: Delete Origin + azure_rm_afdorigin: + name: "{{ origin }}" + origin_group_name: "{{ origin_group }}" + profile_name: "{{ profile }}" + resource_group: "{{ resource_group }}" + state: "absent" + +- name: Delete Origin Group + azure_rm_afdorigingroup: + name: "{{ origin_group }}" + profile_name: "{{ profile }}" + resource_group: "{{ resource_group }}" + state: "absent" + register: output + +- name: Create Ruleset + azure_rm_afdruleset: + name: "{{ ruleset }}" + profile_name: "{{ profile }}" + resource_group: "{{ resource_group }}" + state: "absent" + +- name: Delete EndPoint + azure_rm_afdendpoint: + name: "{{ endpoint }}" + profile_name: "{{ profile }}" + resource_group: "{{ resource_group }}" + state: "absent" + register: output + +- name: Delete Frontdoor Profile + azure_rm_cdnprofile: + name: "{{ profile }}" + location: "Global" + resource_group: "{{ resource_group }}" + sku: "standard_azurefrontdoor" + state: "absent"