diff options
Diffstat (limited to 'ansible_collections/azure/azcollection/plugins/modules')
126 files changed, 7536 insertions, 1277 deletions
diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_accesstoken_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_accesstoken_info.py new file mode 100644 index 000000000..cf9569868 --- /dev/null +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_accesstoken_info.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# +# Copyright (c) 2023 Patrick Uiterwijk <@puiterwijk> +# +# 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 + + +DOCUMENTATION = ''' +--- +module: azure_rm_accesstoken_info + +version_added: "1.19.0" + +short_description: Get Azure API access token + +description: + - Get an access token for Azure APIs. + +options: + scopes: + description: + - The scopes to request. + type: list + elements: str + required: True + claims: + description: + - Additional claims required in the token. + type: list + elements: str + token_tenant_id: + description: + - Tenant to include in the token request. + type: str + enable_cae: + description: + - Whether to enable Continuous Access Evaluation (CAE) for the requested token. + default: false + type: bool + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - Patrick Uiterwijk (@puiterwijk) +''' + +EXAMPLES = ''' +- name: Get access token for Microsoft Graph + azure.azcollection.azure_rm_accesstoken_info: + scopes: + - https://graph.microsoft.com/.default +''' + +RETURN = ''' +access_token: + description: + - API access token. + returned: success + type: str + sample: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.L8i6g3PfcHlioHCCPURC9pmXT7gdJpx3kOoyAfNUwCc +expires_on: + description: + - Timestamp the token expires on. + returned: success + type: int + sample: 1699337824 +''' + + +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + + +class AzureRMAccessToken(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + scopes=dict(type='list', elements='str', required=True), + claims=dict(type='list', elements='str'), + token_tenant_id=dict(type='str'), + enable_cae=dict(type='bool', default=False), + ) + + self.scopes = None + self.claims = None + self.token_tenant_id = None + self.enable_cae = False + + self.results = dict(changed=False) + + super(AzureRMAccessToken, self).__init__(derived_arg_spec=self.module_arg_spec, + supports_check_mode=True, + supports_tags=False, + is_ad_resource=False) + + def exec_module(self, **kwargs): + for key in list(self.module_arg_spec.keys()): + setattr(self, key, kwargs[key]) + + claims = None + if self.claims is not None: + claims = ' '.join(self.claims) + + cred = self.azure_auth.azure_credential_track2 + token = cred.get_token( + *self.scopes, + claims=claims, + tenant_id=self.token_tenant_id, + enable_cae=self.enable_cae, + ) + + self.results['access_token'] = token.token + self.results['expires_on'] = token.expires_on + return self.results + + +def main(): + AzureRMAccessToken() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_account_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_account_info.py index 5061604e2..a3f2109bd 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_account_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_account_info.py @@ -102,14 +102,13 @@ account_info: try: - from azure.graphrbac import GraphRbacManagementClient - from azure.graphrbac.models import GraphErrorException + import asyncio + from msgraph.generated.education.me.user.user_request_builder import UserRequestBuilder except ImportError: # This is handled in azure_rm_common pass from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase -from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMAuth class AzureRMAccountInfo(AzureRMModuleBase): @@ -126,19 +125,15 @@ class AzureRMAccountInfo(AzureRMModuleBase): # Different return info is gathered using 2 different clients # 1. All except "user" section of the return value uses azure.mgmt.subsctiption.operations.subscriptionoperations - # 2. "user" section of the return value uses different client (graphrbac) + # 2. "user" section of the return value uses different client (GraphServiceClient) super(AzureRMAccountInfo, self).__init__(derived_arg_spec=self.module_arg_spec, supports_check_mode=True, supports_tags=False, - is_ad_resource=False) + is_ad_resource=True) def exec_module(self, **kwargs): - - result = [] - result = self.list_items() - - self.results['account_info'] = result + self.results['account_info'] = self.list_items() return self.results def list_items(self): @@ -179,7 +174,7 @@ class AzureRMAccountInfo(AzureRMModuleBase): results['state'] = subscription_list_response[0].state results['managedByTenants'] = self.get_managed_by_tenants_list(subscription_list_response[0].managed_by_tenants) results['environmentName'] = self.azure_auth._cloud_environment.name - results['user'] = self.get_aduser_info(subscription_list_response[0].tenant_id) + results['user'] = self.get_aduser_info() return results @@ -187,33 +182,32 @@ class AzureRMAccountInfo(AzureRMModuleBase): return [dict(tenantId=item.tenant_id) for item in object_list] - def get_aduser_info(self, tenant_id): + def get_aduser_info(self): - # Create GraphRbacManagementClient for getting + # Create GraphServiceClient for getting # "user": { # "name": "mandar123456@abcdefg.onmicrosoft.com", - # "type": "user"self. + # "type": "Member" # } - # Makes use of azure graphrbac - # https://docs.microsoft.com/en-us/python/api/overview/azure/microsoft-graph?view=azure-python#client-library + # Makes use of azure MSGraph + # https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http user = {} - self.azure_auth_graphrbac = AzureRMAuth(is_ad_resource=True) - cred = self.azure_auth_graphrbac.azure_credentials - base_url = self.azure_auth_graphrbac._cloud_environment.endpoints.active_directory_graph_resource_id - client = GraphRbacManagementClient(cred, tenant_id, base_url) - - try: - user_info = client.signed_in_user.get() - user['name'] = user_info.user_principal_name - user['type'] = user_info.object_type - - except GraphErrorException as e: - self.fail("failed to get ad user info {0}".format(str(e))) + user_info = asyncio.get_event_loop().run_until_complete(self.getAccount()) + user['name'] = user_info.user_principal_name + user['type'] = user_info.user_type return user + async def getAccount(self): + return await self.get_msgraph_client().me.get( + request_configuration=UserRequestBuilder.UserRequestBuilderGetRequestConfiguration( + query_parameters=UserRequestBuilder.UserRequestBuilderGetQueryParameters( + select=["userType", "userPrincipalName", "postalCode", "identities"], ), + ) + ) + def main(): AzureRMAccountInfo() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adapplication.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adapplication.py index 9f21728fc..b428463aa 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adapplication.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adapplication.py @@ -5,8 +5,8 @@ # 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 +__metaclass__ = type DOCUMENTATION = ''' --- @@ -20,12 +20,6 @@ description: - Manage Azure Active Directory application. options: - tenant: - description: - - The tenant ID. - type: str - required: True - app_id: description: - Application ID. @@ -34,6 +28,7 @@ options: display_name: description: - The display name of the application. + required: true type: str app_roles: @@ -74,11 +69,24 @@ options: - Any other character, including the space character, are not allowed. type: str - available_to_other_tenants: + sign_in_audience: description: - The application can be used from any Azure AD tenants. - type: bool + - Microsoft Graph SDK deprecate I(available_to_other_tenants), replace by I(sign_in_audience). + - Refer to link U(https://learn.microsoft.com/en-us/graph/migrate-azure-ad-graph-property-differences#application-property-differences) + type: str + choices: + - AzureADMyOrg + - AzureADMultipleOrgs + - AzureADandPersonalMicrosoftAccount + - PersonalMicrosoftAccount + available_to_other_tenants: + description: + - (Deprecated) The application can be used from any Azure AD tenants. + - This parameter was not supported after the migration to Microsoft Graph and was replaced by I(sign_in_audience). + - It will deprecated in next version(V3.0.0). + type: bool credential_description: description: - The description of the password. @@ -170,8 +178,27 @@ options: - App password, aka 'client secret'. type: str - reply_urls: + web_reply_urls: description: + - The web redirect urls. + - Space-separated URIs to which Azure AD will redirect in response to an OAuth 2.0 request. + - The value does not need to be a physical endpoint, but must be a valid URI. + type: list + elements: str + aliases: + - reply_urls + + spa_reply_urls: + description: + - The spa redirect urls. + - Space-separated URIs to which Azure AD will redirect in response to an OAuth 2.0 request. + - The value does not need to be a physical endpoint, but must be a valid URI. + type: list + elements: str + + public_client_reply_urls: + description: + - The public client redirect urls. - Space-separated URIs to which Azure AD will redirect in response to an OAuth 2.0 request. - The value does not need to be a physical endpoint, but must be a valid URI. type: list @@ -236,14 +263,24 @@ author: EXAMPLES = ''' - name: Create ad application azure_rm_adapplication: - tenant: "{{ tenant_id }}" display_name: "{{ display_name }}" +- name: Create ad application with multi redirect urls + azure_rm_adapplication: + display_name: "{{ display_name }}" + web_reply_urls: + - https://web01.com + spa_reply_urls: + - https://spa01.com + - https://spa02.com + public_client_reply_urls: + - https://public01.com + - https://public02.com + - name: Create application with more parameter azure_rm_adapplication: - tenant: "{{ tenant_id }}" display_name: "{{ display_name }}" - available_to_other_tenants: false + sign_in_audience: AzureADandPersonalMicrosoftAccount credential_description: "for test" end_date: 2021-10-01 start_date: 2021-05-18 @@ -251,7 +288,6 @@ EXAMPLES = ''' - name: delete ad application azure_rm_adapplication: - tenant: "{{ tenant_id }}" app_id: "{{ app_id }}" state: absent ''' @@ -281,12 +317,18 @@ output: returned: always type: str sample: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + sign_in_audience: + description: + - The application can be used from any Azure AD tenants. + returned: always + type: str + sample: AzureADandPersonalMicrosoftAccount available_to_other_tenants: description: - The application can be used from any Azure AD tenants. returned: always - type: bool - sample: false + type: str + sample: AzureADandPersonalMicrosoftAccount homepage: description: - The url where users can sign in and use your app. @@ -311,8 +353,23 @@ output: returned: always type: list sample: [] - reply_urls: + public_client_reply_urls: + description: + - The public client redirect urls. + - Space-separated URIs to which Azure AD will redirect in response to an OAuth 2.0 request. + returned: always + type: list + sample: [] + web_reply_urls: description: + - The web redirect urls. + - Space-separated URIs to which Azure AD will redirect in response to an OAuth 2.0 request. + returned: always + type: list + sample: [] + spa_reply_urls: + description: + - The spa redirect urls. - Space-separated URIs to which Azure AD will redirect in response to an OAuth 2.0 request. returned: always type: list @@ -322,17 +379,22 @@ output: from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBaseExt try: - from azure.graphrbac.models import GraphErrorException import datetime - from dateutil.relativedelta import relativedelta import dateutil.parser - from azure.graphrbac.models import ApplicationCreateParameters import uuid - from azure.graphrbac.models import ResourceAccess - from azure.graphrbac.models import RequiredResourceAccess - from azure.graphrbac.models import AppRole - from azure.graphrbac.models import PasswordCredential, KeyCredential - from azure.graphrbac.models import ApplicationUpdateParameters + import asyncio + from dateutil.relativedelta import relativedelta + from msgraph.generated.applications.applications_request_builder import ApplicationsRequestBuilder + from msgraph.generated.models.application import Application + from msgraph.generated.models.password_credential import PasswordCredential + from msgraph.generated.models.key_credential import KeyCredential + from msgraph.generated.models.required_resource_access import RequiredResourceAccess + from msgraph.generated.models.resource_access import ResourceAccess + from msgraph.generated.models.app_role import AppRole + from msgraph.generated.models.web_application import WebApplication + from msgraph.generated.models.spa_application import SpaApplication + from msgraph.generated.models.public_client_application import PublicClientApplication + from msgraph.generated.models.implicit_grant_settings import ImplicitGrantSettings except ImportError: # This is handled in azure_rm_common pass @@ -396,31 +458,40 @@ class AzureRMADApplication(AzureRMModuleBaseExt): def __init__(self): self.module_arg_spec = dict( - tenant=dict(type='str', required=True), app_id=dict(type='str'), - display_name=dict(type='str'), + display_name=dict(type='str', required=True), app_roles=dict(type='list', elements='dict', options=app_role_spec), - available_to_other_tenants=dict(type='bool'), + sign_in_audience=dict( + type='str', + choices=['AzureADMyOrg', 'AzureADMultipleOrgs', 'AzureADandPersonalMicrosoftAccount', 'PersonalMicrosoftAccount'] + ), + available_to_other_tenants=dict( + type='bool', + removed_in_version='3.0.0', + removed_from_collection='azure.azcollection' + ), credential_description=dict(type='str'), end_date=dict(type='str'), homepage=dict(type='str'), allow_guests_sign_in=dict(type='bool'), identifier_uris=dict(type='list', elements='str'), - key_type=dict(type='str', default='AsymmetricX509Cert', choices=['AsymmetricX509Cert', 'Password', 'Symmetric']), + key_type=dict(type='str', default='AsymmetricX509Cert', + choices=['AsymmetricX509Cert', 'Password', 'Symmetric']), key_usage=dict(type='str', default='Verify', choices=['Sign', 'Verify']), key_value=dict(type='str', no_log=True), native_app=dict(type='bool'), oauth2_allow_implicit_flow=dict(type='bool'), optional_claims=dict(type='list', elements='dict', options=optional_claims_spec), password=dict(type='str', no_log=True), - reply_urls=dict(type='list', elements='str'), + public_client_reply_urls=dict(type='list', elements='str'), + web_reply_urls=dict(type='list', elements='str', aliases=['reply_urls']), + spa_reply_urls=dict(type='list', elements='str'), start_date=dict(type='str'), required_resource_accesses=dict(type='list', elements='dict', options=required_resource_accesses_spec), state=dict(type='str', default='present', choices=['present', 'absent']), ) self.state = None - self.tenant = None self.app_id = None self.display_name = None self.app_roles = None @@ -436,11 +507,15 @@ class AzureRMADApplication(AzureRMModuleBaseExt): self.oauth2_allow_implicit_flow = None self.optional_claims = None self.password = None - self.reply_urls = None + self.public_client_reply_urls = None + self.spa_reply_urls = None + self.web_reply_urls = None self.start_date = None self.required_resource_accesses = None self.allow_guests_sign_in = None self.results = dict(changed=False) + self._client = None + self.sign_in_audience = None super(AzureRMADApplication, self).__init__(derived_arg_spec=self.module_arg_spec, supports_check_mode=False, @@ -448,12 +523,11 @@ class AzureRMADApplication(AzureRMModuleBaseExt): is_ad_resource=True) def exec_module(self, **kwargs): - + self._client = self.get_msgraph_client() for key in list(self.module_arg_spec.keys()): setattr(self, key, kwargs[key]) response = self.get_resource() - if response: if self.state == 'present': if self.check_update(response): @@ -475,90 +549,113 @@ class AzureRMADApplication(AzureRMModuleBaseExt): if self.identifier_uris: self.fail("'identifier_uris' is not required for creating a native application") else: - password_creds, key_creds = self.build_application_creds(self.password, self.key_value, self.key_type, self.key_usage, - self.start_date, self.end_date, self.credential_description) + password_creds, key_creds = self.build_application_creds(self.password, self.key_value, self.key_type, + self.key_usage, + self.start_date, self.end_date, + self.credential_description) if self.required_resource_accesses: required_accesses = self.build_application_accesses(self.required_resource_accesses) if self.app_roles: app_roles = self.build_app_roles(self.app_roles) - client = self.get_graphrbac_client(self.tenant) - app_create_param = ApplicationCreateParameters(available_to_other_tenants=self.available_to_other_tenants, - display_name=self.display_name, - identifier_uris=self.identifier_uris, - homepage=self.homepage, - reply_urls=self.reply_urls, - key_credentials=key_creds, - password_credentials=password_creds, - oauth2_allow_implicit_flow=self.oauth2_allow_implicit_flow, - required_resource_access=required_accesses, - app_roles=app_roles, - allow_guests_sign_in=self.allow_guests_sign_in, - optional_claims=self.optional_claims) - response = client.applications.create(app_create_param) + create_app = Application( + sign_in_audience=self.sign_in_audience, + web=WebApplication( + home_page_url=self.homepage, + redirect_uris=self.web_reply_urls, + implicit_grant_settings=ImplicitGrantSettings( + enable_access_token_issuance=self.oauth2_allow_implicit_flow, + ), + ), + spa=SpaApplication(redirect_uris=self.spa_reply_urls), + public_client=PublicClientApplication(redirect_uris=self.public_client_reply_urls), + display_name=self.display_name, + identifier_uris=self.identifier_uris, + key_credentials=key_creds, + password_credentials=password_creds, + required_resource_access=required_accesses, + app_roles=app_roles, + optional_claims=self.optional_claims + # allow_guests_sign_in=self.allow_guests_sign_in, + ) + response = asyncio.get_event_loop().run_until_complete(self.create_application(create_app)) self.results['changed'] = True self.results.update(self.to_dict(response)) return response - except GraphErrorException as ge: - self.fail("Error creating application, display_name {0} - {1}".format(self.display_name, str(ge))) + except Exception as ge: + self.fail("Error creating application, display_name: {0} - {1}".format(self.display_name, str(ge))) def update_resource(self, old_response): try: - client = self.get_graphrbac_client(self.tenant) key_creds, password_creds, required_accesses, app_roles, optional_claims = None, None, None, None, None if self.native_app: if self.identifier_uris: self.fail("'identifier_uris' is not required for creating a native application") else: - password_creds, key_creds = self.build_application_creds(self.password, self.key_value, self.key_type, self.key_usage, - self.start_date, self.end_date, self.credential_description) + password_creds, key_creds = self.build_application_creds(self.password, self.key_value, self.key_type, + self.key_usage, + self.start_date, self.end_date, + self.credential_description) if self.required_resource_accesses: required_accesses = self.build_application_accesses(self.required_resource_accesses) if self.app_roles: app_roles = self.build_app_roles(self.app_roles) - app_update_param = ApplicationUpdateParameters(available_to_other_tenants=self.available_to_other_tenants, - display_name=self.display_name, - identifier_uris=self.identifier_uris, - homepage=self.homepage, - reply_urls=self.reply_urls, - key_credentials=key_creds, - password_credentials=password_creds, - oauth2_allow_implicit_flow=self.oauth2_allow_implicit_flow, - required_resource_access=required_accesses, - allow_guests_sign_in=self.allow_guests_sign_in, - app_roles=app_roles, - optional_claims=self.optional_claims) - client.applications.patch(old_response['object_id'], app_update_param) + + app_update_param = Application( + sign_in_audience=self.sign_in_audience, + web=WebApplication( + home_page_url=self.homepage, + redirect_uris=self.web_reply_urls, + implicit_grant_settings=ImplicitGrantSettings( + enable_access_token_issuance=self.oauth2_allow_implicit_flow, + ), + ), + spa=SpaApplication(redirect_uris=self.spa_reply_urls), + public_client=PublicClientApplication(redirect_uris=self.public_client_reply_urls), + display_name=self.display_name, + identifier_uris=self.identifier_uris, + key_credentials=key_creds, + password_credentials=password_creds, + required_resource_access=required_accesses, + # allow_guests_sign_in=self.allow_guests_sign_in, + app_roles=app_roles, + optional_claims=self.optional_claims) + asyncio.get_event_loop().run_until_complete(self.update_application( + obj_id=old_response['object_id'], update_app=app_update_param)) + self.results['changed'] = True self.results.update(self.get_resource()) - except GraphErrorException as ge: + except Exception as ge: self.fail("Error updating the application app_id {0} - {1}".format(self.app_id, str(ge))) def delete_resource(self, response): try: - client = self.get_graphrbac_client(self.tenant) - client.applications.delete(response.get('object_id')) + asyncio.get_event_loop().run_until_complete(self.delete_application(response.get('object_id'))) self.results['changed'] = True return True - except GraphErrorException as ge: - self.fail("Error deleting application app_id {0} display_name {1} - {2}".format(self.app_id, self.display_name, str(ge))) + except Exception as ge: + self.fail( + "Error deleting application app_id {0} display_name {1} - {2}".format(self.app_id, self.display_name, + str(ge))) def get_resource(self): try: - client = self.get_graphrbac_client(self.tenant) existing_apps = [] if self.app_id: - existing_apps = list(client.applications.list(filter="appId eq '{0}'".format(self.app_id))) + ret = asyncio.get_event_loop().run_until_complete(self.get_application_by_app_id(self.app_id)) + existing_apps = ret.value + if not existing_apps: return False result = existing_apps[0] return self.to_dict(result) - except GraphErrorException as ge: - self.log("Did not find the graph instance instance {0} - {1}".format(self.app_id, str(ge))) - return False + except Exception as ge: + self.fail(ge) + # self.log("Did not find the graph instance instance {0} - {1}".format(self.app_id, str(ge))) + # return False def check_update(self, response): for key in list(self.module_arg_spec.keys()): @@ -575,19 +672,22 @@ class AzureRMADApplication(AzureRMModuleBaseExt): 'is_enabled': app_role.is_enabled, 'value': app_role.value, "description": app_role.description - }for app_role in object.app_roles] + } for app_role in object.app_roles] return dict( app_id=object.app_id, - object_id=object.object_id, + object_id=object.id, display_name=object.display_name, app_roles=app_roles, - available_to_other_tenants=object.available_to_other_tenants, - homepage=object.homepage, + available_to_other_tenants=object.sign_in_audience, + sign_in_audience=object.sign_in_audience, + homepage=object.web.home_page_url, identifier_uris=object.identifier_uris, - oauth2_allow_implicit_flow=object.oauth2_allow_implicit_flow, + oauth2_allow_implicit_flow=object.web.implicit_grant_settings.enable_access_token_issuance, optional_claims=object.optional_claims, - allow_guests_sign_in=object.allow_guests_sign_in, - reply_urls=object.reply_urls + # allow_guests_sign_in=object.allow_guests_sign_in, + web_reply_urls=object.web.redirect_uris, + spa_reply_urls=object.spa.redirect_uris, + public_client_reply_urls=object.public_client.redirect_uris ) def build_application_creds(self, password=None, key_value=None, key_type=None, key_usage=None, @@ -615,14 +715,16 @@ class AzureRMADApplication(AzureRMModuleBaseExt): password_creds = None key_creds = None if password: - password_creds = [PasswordCredential(start_date=start_date, end_date=end_date, key_id=str(self.gen_guid()), - value=password, custom_key_identifier=custom_key_id)] + password_creds = [PasswordCredential(start_date_time=start_date, end_date_time=end_date, + key_id=self.gen_guid(), secret_text=password, + custom_key_identifier=custom_key_id)] # value ? secret_text elif key_value: key_creds = [ - KeyCredential(start_date=start_date, end_date=end_date, key_id=str(self.gen_guid()), value=key_value, + KeyCredential(start_date_time=start_date, end_date_time=end_date, key_id=self.gen_guid(), key=key_value, + # value ? key usage=key_usage, type=key_type, custom_key_identifier=custom_key_id)] - return (password_creds, key_creds) + return password_creds, key_creds def encode_custom_key_description(self, key_description): # utf16 is used by AAD portal. Do not change it to other random encoding @@ -640,7 +742,6 @@ class AzureRMADApplication(AzureRMModuleBaseExt): self.log('Getting "requiredResourceAccess" from a full manifest') required_resource_accesses = required_resource_accesses.get('required_resource_access', []) for x in required_resource_accesses: - accesses = [ResourceAccess(id=y['id'], type=y['type']) for y in x['resource_access']] required_accesses.append(RequiredResourceAccess(resource_app_id=x['resource_app_id'], resource_access=accesses)) @@ -657,10 +758,34 @@ class AzureRMADApplication(AzureRMModuleBaseExt): role = AppRole(id=x.get('id', None) or self.gen_guid(), allowed_member_types=x.get('allowed_member_types', None), description=x.get('description', None), display_name=x.get('display_name', None), - is_enabled=x.get('is_enabled', None), value=x.get('value', None)) + is_enabled=x.get('is_enabled', None), value=x.get('value', None)) # value ? additional_data result.append(role) return result + async def create_application(self, creat_app): + return await self._client.applications.post(body=creat_app) + + async def update_application(self, obj_id, update_app): + return await self._client.applications.by_application_id(obj_id).patch(body=update_app) + + async def get_application_by_app_id(self, app_id): + request_configuration = ApplicationsRequestBuilder.ApplicationsRequestBuilderGetRequestConfiguration( + query_parameters=ApplicationsRequestBuilder.ApplicationsRequestBuilderGetQueryParameters( + filter=(" appId eq '{0}'".format(app_id)), ), + ) + + return await self._client.applications.get(request_configuration=request_configuration) + + async def delete_application(self, obj_id): + await self._client.applications.by_application_id(obj_id).delete() + + async def get_applications(self, filters): + request_configuration = ApplicationsRequestBuilder.ApplicationsRequestBuilderGetRequestConfiguration( + query_parameters=ApplicationsRequestBuilder.ApplicationsRequestBuilderGetQueryParameters( + filter=(' and '.join(filters)) + )) + return await self._client.applications.get(request_configuration=request_configuration) + def main(): AzureRMADApplication() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adapplication_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adapplication_info.py index 939058815..167b82552 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adapplication_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adapplication_info.py @@ -6,8 +6,8 @@ from __future__ import absolute_import, division, print_function -__metaclass__ = type +__metaclass__ = type DOCUMENTATION = ''' module: azure_rm_adapplication_info @@ -24,18 +24,17 @@ options: description: - The application ID. type: str - tenant: - description: - - The tenant ID. - type: str - required: True object_id: description: - - It's application's object ID. + - The application's object ID. type: str identifier_uri: description: - - It's identifier_uri's object ID. + - The identifier_uri's object ID. + type: str + app_display_name: + description: + - The applications' Name. type: str extends_documentation_fragment: @@ -45,23 +44,25 @@ author: haiyuan_zhang (@haiyuazhang) Fred-sun (@Fred-sun) guopeng_lin (@guopenglin) + Xu Zhang (@xuzhang) ''' EXAMPLES = ''' - name: get ad app info by App ID azure_rm_adapplication_info: app_id: "{{ app_id }}" - tenant: "{{ tenant_id }}" - name: get ad app info ---- by object ID azure_rm_adapplication_info: object_id: "{{ object_id }}" - tenant: "{{ tenant_id }}" - name: get ad app info ---- by identifier uri azure_rm_adapplication_info: identifier_uri: "{{ identifier_uri }}" - tenant: "{{ tenant_id }}" + +- name: get ad app info ---- by display name + azure_rm_adapplication_info: + app_display_name: "{{ display_name }}" ''' RETURN = ''' @@ -95,12 +96,47 @@ applications: returned: always type: str sample: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + sign_in_audience: + description: + - The application can be used from any Azure AD tenants + type: str + returned: always + sample: AzureADandPersonalMicrosoftAccount + available_to_other_tenants: + description: + - The application can be used from any Azure AD tenants + type: str + returned: always + sample: AzureADandPersonalMicrosoftAccount + public_client_reply_urls: + description: + - The public client redirect urls. + - Space-separated URIs to which Azure AD will redirect in response to an OAuth 2.0 request. + returned: always + type: list + sample: [] + web_reply_urls: + description: + - The web redirect urls. + - Space-separated URIs to which Azure AD will redirect in response to an OAuth 2.0 request. + returned: always + type: list + sample: [] + spa_reply_urls: + description: + - The spa redirect urls. + - Space-separated URIs to which Azure AD will redirect in response to an OAuth 2.0 request. + returned: always + type: list + sample: [] ''' from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBase try: - from azure.graphrbac.models import GraphErrorException + import asyncio + from msgraph.generated.applications.applications_request_builder import ApplicationsRequestBuilder + from kiota_abstractions.api_error import APIError except ImportError: # This is handled in azure_rm_common pass @@ -110,25 +146,17 @@ class AzureRMADApplicationInfo(AzureRMModuleBase): def __init__(self): self.module_arg_spec = dict( - app_id=dict( - type='str' - ), - object_id=dict( - type='str' - ), - identifier_uri=dict( - type='str' - ), - tenant=dict( - type='str', - required=True - ) + app_id=dict(type='str'), + object_id=dict(type='str'), + identifier_uri=dict(type='str'), + app_display_name=dict(type='str') ) - self.tenant = None self.app_id = None + self.app_display_name = None self.object_id = None self.identifier_uri = None self.results = dict(changed=False) + self._client = None super(AzureRMADApplicationInfo, self).__init__(derived_arg_spec=self.module_arg_spec, supports_check_mode=True, supports_tags=False, @@ -139,21 +167,26 @@ class AzureRMADApplicationInfo(AzureRMModuleBase): setattr(self, key, kwargs[key]) applications = [] + try: - client = self.get_graphrbac_client(self.tenant) + self._client = self.get_msgraph_client() if self.object_id: - applications = [client.applications.get(self.object_id)] + applications = [asyncio.get_event_loop().run_until_complete(self.get_application(self.object_id))] else: sub_filters = [] if self.identifier_uri: sub_filters.append("identifierUris/any(s:s eq '{0}')".format(self.identifier_uri)) if self.app_id: sub_filters.append("appId eq '{0}'".format(self.app_id)) - # applications = client.applications.list(filter=(' and '.join(sub_filters))) - applications = list(client.applications.list(filter=(' and '.join(sub_filters)))) - + if self.app_display_name: + sub_filters.append("displayName eq '{0}'".format(self.app_display_name)) + apps = asyncio.get_event_loop().run_until_complete(self.get_applications(sub_filters)) + applications = list(apps) self.results['applications'] = [self.to_dict(app) for app in applications] - except GraphErrorException as ge: + except APIError as e: + if e.response_status_code != 404: + self.fail("failed to get application info {0}".format(str(e))) + except Exception as ge: self.fail("failed to get application info {0}".format(str(ge))) return self.results @@ -161,11 +194,46 @@ class AzureRMADApplicationInfo(AzureRMModuleBase): def to_dict(self, object): return dict( app_id=object.app_id, - object_id=object.object_id, + object_id=object.id, app_display_name=object.display_name, - identifier_uris=object.identifier_uris + identifier_uris=object.identifier_uris, + available_to_other_tenants=object.sign_in_audience, + sign_in_audience=object.sign_in_audience, + web_reply_urls=object.web.redirect_uris, + spa_reply_urls=object.spa.redirect_uris, + public_client_reply_urls=object.public_client.redirect_uris ) + async def get_application(self, obj_id): + return await self._client.applications.by_application_id(obj_id).get() + + async def get_applications(self, sub_filters): + if sub_filters: + request_configuration = ApplicationsRequestBuilder.ApplicationsRequestBuilderGetRequestConfiguration( + query_parameters=ApplicationsRequestBuilder.ApplicationsRequestBuilderGetQueryParameters( + filter=(' and '.join(sub_filters)), + ), + ) + applications = await self._client.applications.get(request_configuration=request_configuration) + return applications.value + else: + applications_list = [] + applications = await self._client.applications.get() + for app in applications.value: + applications_list.append(app) + + if applications.odata_next_link: + next_link = applications.odata_next_link + else: + next_link = None + + while next_link: + applications = await self._client.applications.with_url(next_link).get() + next_link = applications.odata_next_link + for app in applications.value: + applications_list.append(app) + return applications_list + def main(): AzureRMADApplicationInfo() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adgroup.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adgroup.py index 812b6953c..1693794a7 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adgroup.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adgroup.py @@ -5,6 +5,7 @@ # 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 DOCUMENTATION = ''' @@ -14,11 +15,6 @@ short_description: Manage Azure Active Directory group description: - Create, update or delete Azure Active Directory group. options: - tenant: - description: - - The tenant ID. - type: str - required: True state: description: - Assert the state of the resource group. Use C(present) to create or update and C(absent) to delete. @@ -67,6 +63,10 @@ options: - The azure ad objects asserted to not be owners of the group. type: list elements: str + description: + description: + - An optional description for the group. + type: str extends_documentation_fragment: - azure.azcollection.azure author: @@ -76,46 +76,41 @@ author: EXAMPLES = ''' - name: Create Group azure_rm_adgroup: - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx display_name: "Group-Name" mail_nickname: "Group-Mail-Nickname" + description: 'fortest' state: 'present' - name: Delete Group using display_name and mail_nickname azure_rm_adgroup: - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx display_name: "Group-Name" mail_nickname: "Group-Mail-Nickname" state: 'absent' - name: Delete Group using object_id azure_rm_adgroup: - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx state: 'absent' - name: Ensure Users are Members of a Group using display_name and mail_nickname azure_rm_adgroup: - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx display_name: "Group-Name" mail_nickname: "Group-Mail-Nickname" state: 'present' present_members: - - "https://graph.windows.net/{{ tenant_id }}/directoryObjects/{{ ad_object_1_object_id }}" - - "https://graph.windows.net/{{ tenant_id }}/directoryObjects/{{ ad_object_2_object_id }}" + - "{{ ad_object_1_object_id }}" + - "{{ ad_object_2_object_id }}" - name: Ensure Users are Members of a Group using object_id azure_rm_adgroup: - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx state: 'present' present_members: - - "https://graph.windows.net/{{ ad_object_1_tenant_id }}/directoryObjects/{{ ad_object_1_object_id }}" - - "https://graph.windows.net/{{ ad_object_2_tenant_id }}/directoryObjects/{{ ad_object_2_object_id }}" + - "{{ ad_object_1_object_id }}" + - "{{ ad_object_2_object_id }}" - name: Ensure Users are not Members of a Group using display_name and mail_nickname azure_rm_adgroup: - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx display_name: "Group-Name" mail_nickname: "Group-Mail-Nickname" state: 'present' @@ -124,7 +119,6 @@ EXAMPLES = ''' - name: Ensure Users are Members of a Group using object_id azure_rm_adgroup: - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx state: 'present' absent_members: @@ -132,26 +126,23 @@ EXAMPLES = ''' - name: Ensure Users are Owners of a Group using display_name and mail_nickname azure_rm_adgroup: - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx display_name: "Group-Name" mail_nickname: "Group-Mail-Nickname" state: 'present' present_owners: - - "https://graph.windows.net/{{ tenant_id }}/directoryObjects/{{ ad_object_1_object_id }}" - - "https://graph.windows.net/{{ tenant_id }}/directoryObjects/{{ ad_object_2_object_id }}" + - "{{ ad_object_1_object_id }}" + - "{{ ad_object_2_object_id }}" - name: Ensure Users are Owners of a Group using object_id azure_rm_adgroup: - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx state: 'present' present_owners: - - "https://graph.windows.net/{{ ad_object_1_tenant_id }}/directoryObjects/{{ ad_object_1_object_id }}" - - "https://graph.windows.net/{{ ad_object_2_tenant_id }}/directoryObjects/{{ ad_object_2_object_id }}" + - "{{ ad_object_1_object_id }}" + - "{{ ad_object_2_object_id }}" - name: Ensure Users are not Owners of a Group using display_name and mail_nickname azure_rm_adgroup: - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx display_name: "Group-Name" mail_nickname: "Group-Mail-Nickname" state: 'present' @@ -161,7 +152,6 @@ EXAMPLES = ''' - name: Ensure Users are Owners of a Group using object_id azure_rm_adgroup: - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx state: 'present' absent_owners: @@ -216,13 +206,23 @@ group_members: - The members of the group. returned: always type: list +description: + description: + - An optional description for the group. + type: str + returned: always + sample: 'fortest' ''' from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBase try: - from azure.graphrbac.models import GraphErrorException - from azure.graphrbac.models import GroupCreateParameters + import asyncio + from msgraph.generated.groups.groups_request_builder import GroupsRequestBuilder + from msgraph.generated.models.group import Group + from msgraph.generated.groups.item.transitive_members.transitive_members_request_builder import \ + TransitiveMembersRequestBuilder + from msgraph.generated.models.reference_create import ReferenceCreate except ImportError: # This is handled in azure_rm_common pass @@ -239,7 +239,7 @@ class AzureRMADGroup(AzureRMModuleBase): present_owners=dict(type='list', elements='str'), absent_members=dict(type='list', elements='str'), absent_owners=dict(type='list', elements='str'), - tenant=dict(type='str', required=True), + description=dict(type='str'), state=dict( type='str', default='present', @@ -247,7 +247,6 @@ class AzureRMADGroup(AzureRMModuleBase): ), ) - self.tenant = None self.display_name = None self.mail_nickname = None self.object_id = None @@ -257,6 +256,7 @@ class AzureRMADGroup(AzureRMModuleBase): self.absent_owners = [] self.state = None self.results = dict(changed=False) + self._client = None super(AzureRMADGroup, self).__init__(derived_arg_spec=self.module_arg_spec, supports_check_mode=False, @@ -264,7 +264,6 @@ class AzureRMADGroup(AzureRMModuleBase): is_ad_resource=True) def exec_module(self, **kwargs): - for key in list(self.module_arg_spec.keys()): setattr(self, key, kwargs[key]) @@ -272,53 +271,61 @@ class AzureRMADGroup(AzureRMModuleBase): ad_groups = [] try: - client = self.get_graphrbac_client(self.tenant) + self._client = self.get_msgraph_client() ad_groups = [] if self.display_name and self.mail_nickname: - ad_groups = list(client.groups.list(filter="displayName eq '{0}' and mailNickname eq '{1}'".format(self.display_name, self.mail_nickname))) - + filter = "displayName eq '{0}' and mailNickname eq '{1}'".format(self.display_name, self.mail_nickname) + ad_groups = asyncio.get_event_loop().run_until_complete(self.get_group_list(filter)) if ad_groups: - self.object_id = ad_groups[0].object_id + self.object_id = ad_groups[0].id elif self.object_id: - ad_groups = [client.groups.get(self.object_id)] + ad_groups = [asyncio.get_event_loop().run_until_complete(self.get_group(self.object_id))] if ad_groups: if self.state == "present": self.results["changed"] = False elif self.state == "absent": - ad_groups = [client.groups.delete(self.object_id)] + asyncio.get_event_loop().run_until_complete(self.delete_group(self.object_id)) + ad_groups = [] self.results["changed"] = True else: if self.state == "present": if self.display_name and self.mail_nickname: - ad_groups = [client.groups.create(GroupCreateParameters(display_name=self.display_name, mail_nickname=self.mail_nickname))] + group = Group( + mail_enabled=False, + security_enabled=True, + group_types=[], + display_name=self.display_name, + mail_nickname=self.mail_nickname, + description=self.description + ) + + ad_groups = [asyncio.get_event_loop().run_until_complete(self.create_group(group))] self.results["changed"] = True else: - raise ValueError('The group does not exist. Both display_name : {0} and mail_nickname : {1} must be passed to create a new group' - .format(self.display_name, self.mail_nickname)) + raise ValueError( + 'The group does not exist. Both display_name : {0} and mail_nickname : {1} must be passed to create a new group' + .format(self.display_name, self.mail_nickname)) elif self.state == "absent": self.results["changed"] = False - if ad_groups[0] is not None: - self.update_members(ad_groups[0].object_id, client) - self.update_owners(ad_groups[0].object_id, client) - self.results.update(self.set_results(ad_groups[0], client)) + if ad_groups and ad_groups[0] is not None: + self.update_members(ad_groups[0].id) + self.update_owners(ad_groups[0].id) + self.results.update(self.set_results(ad_groups[0])) - except GraphErrorException as e: - self.fail(e) - except ValueError as e: + except Exception as e: self.fail(e) - return self.results - def update_members(self, group_id, client): - + def update_members(self, group_id): current_members = [] if self.present_members or self.absent_members: - current_members = [object.object_id for object in list(client.groups.get_group_members(group_id))] + ret = asyncio.get_event_loop().run_until_complete(self.get_group_members(group_id)) + current_members = [object.id for object in ret.value] if self.present_members: present_members_by_object_id = self.dictionary_from_object_urls(self.present_members) @@ -327,8 +334,8 @@ class AzureRMADGroup(AzureRMModuleBase): if members_to_add: for member_object_id in members_to_add: - client.groups.add_member(group_id, present_members_by_object_id[member_object_id]) - + asyncio.get_event_loop().run_until_complete( + self.add_group_member(group_id, present_members_by_object_id[member_object_id])) self.results["changed"] = True if self.absent_members: @@ -336,24 +343,25 @@ class AzureRMADGroup(AzureRMModuleBase): if members_to_remove: for member in members_to_remove: - client.groups.remove_member(group_id, member) + asyncio.get_event_loop().run_until_complete(self.delete_group_member(group_id, member)) self.results["changed"] = True - def update_owners(self, group_id, client): + def update_owners(self, group_id): current_owners = [] if self.present_owners or self.absent_owners: - current_owners = [object.object_id for object in list(client.groups.list_owners(group_id))] + ret = asyncio.get_event_loop().run_until_complete(self.get_group_owners(group_id)) + current_owners = [object.id for object in ret.value] if self.present_owners: present_owners_by_object_id = self.dictionary_from_object_urls(self.present_owners) - owners_to_add = list(set(present_owners_by_object_id.keys()) - set(current_owners)) if owners_to_add: for owner_object_id in owners_to_add: - client.groups.add_owner(group_id, present_owners_by_object_id[owner_object_id]) + asyncio.get_event_loop().run_until_complete( + self.add_gropup_owner(group_id, present_owners_by_object_id[owner_object_id])) self.results["changed"] = True if self.absent_owners: @@ -361,7 +369,7 @@ class AzureRMADGroup(AzureRMModuleBase): if owners_to_remove: for owner in owners_to_remove: - client.groups.remove_owner(group_id, owner) + asyncio.get_event_loop().run_until_complete(self.remove_gropup_owner(group_id, owner)) self.results["changed"] = True def dictionary_from_object_urls(self, object_urls): @@ -383,24 +391,25 @@ class AzureRMADGroup(AzureRMModuleBase): def serviceprincipal_to_dict(self, object): return dict( app_id=object.app_id, - object_id=object.object_id, + object_id=object.id, app_display_name=object.display_name, app_role_assignment_required=object.app_role_assignment_required ) def group_to_dict(self, object): return dict( - object_id=object.object_id, + object_id=object.id, display_name=object.display_name, mail_nickname=object.mail_nickname, mail_enabled=object.mail_enabled, security_enabled=object.security_enabled, - mail=object.mail + mail=object.mail, + description=object.description ) def user_to_dict(self, object): return dict( - object_id=object.object_id, + object_id=object.id, display_name=object.display_name, user_principal_name=object.user_principal_name, mail_nickname=object.mail_nickname, @@ -410,28 +419,92 @@ class AzureRMADGroup(AzureRMModuleBase): ) def result_to_dict(self, object): - if object.object_type == "Group": + if object.odata_type == "#microsoft.graph.group": return self.group_to_dict(object) - elif object.object_type == "User": + elif object.odata_type == "#microsoft.graph.user": return self.user_to_dict(object) - elif object.object_type == "Application": + elif object.odata_type == "#microsoft.graph.application": return self.application_to_dict(object) - elif object.object_type == "ServicePrincipal": + elif object.odata_type == "#microsoft.graph.servicePrincipal": return self.serviceprincipal_to_dict(object) else: - return object.object_type + return object.odata_type - def set_results(self, object, client): + def set_results(self, object): results = self.group_to_dict(object) if results["object_id"] and (self.present_owners or self.absent_owners): - results["group_owners"] = [self.result_to_dict(object) for object in list(client.groups.list_owners(results["object_id"]))] + ret = asyncio.get_event_loop().run_until_complete(self.get_group_owners(results["object_id"])) + results["group_owners"] = [self.result_to_dict(object) for object in ret.value] if results["object_id"] and (self.present_members or self.absent_members): - results["group_members"] = [self.result_to_dict(object) for object in list(client.groups.get_group_members(results["object_id"]))] + ret = asyncio.get_event_loop().run_until_complete(self.get_group_members(results["object_id"])) + results["group_members"] = [self.result_to_dict(object) for object in ret.value] return results + async def create_group(self, create_group): + return await self._client.groups.post(body=create_group) + + async def delete_group(self, group_id): + await self._client.groups.by_group_id(group_id).delete() + + async def get_group(self, group_id): + return await self._client.groups.by_group_id(group_id).get() + + async def get_group_list(self, filter=None): + if filter: + request_configuration = GroupsRequestBuilder.GroupsRequestBuilderGetRequestConfiguration( + query_parameters=GroupsRequestBuilder.GroupsRequestBuilderGetQueryParameters( + count=True, + filter=filter, + ), + ) + groups = await self._client.groups.get(request_configuration=request_configuration) + else: + groups = await self._client.groups.get() + + if groups and groups.value: + return groups.value + return [] + + async def get_group_members(self, group_id, filters=None): + request_configuration = TransitiveMembersRequestBuilder.TransitiveMembersRequestBuilderGetRequestConfiguration( + query_parameters=TransitiveMembersRequestBuilder.TransitiveMembersRequestBuilderGetQueryParameters( + count=True, + ), + ) + if filters: + request_configuration.query_parameters.filter = filters + return await self._client.groups.by_group_id(group_id).transitive_members.get( + request_configuration=request_configuration) + + async def add_group_member(self, group_id, obj_id): + request_body = ReferenceCreate( + odata_id="https://graph.microsoft.com/v1.0/directoryObjects/{0}".format(obj_id), + ) + await self._client.groups.by_group_id(group_id).members.ref.post(body=request_body) + + async def delete_group_member(self, group_id, member_id): + await self._client.groups.by_group_id(group_id).members.by_directory_object_id(member_id).ref.delete() + + async def get_group_owners(self, group_id): + request_configuration = GroupsRequestBuilder.GroupsRequestBuilderGetRequestConfiguration( + query_parameters=GroupsRequestBuilder.GroupsRequestBuilderGetQueryParameters( + count=True, + ), + ) + return await self._client.groups.by_group_id(group_id).owners.get(request_configuration=request_configuration) + + async def add_gropup_owner(self, group_id, obj_id): + request_body = ReferenceCreate( + odata_id="https://graph.microsoft.com/v1.0/users/{0}".format(obj_id), + ) + await self._client.groups.by_group_id(group_id).owners.ref.post(body=request_body) + + async def remove_gropup_owner(self, group_id, obj_id): + await self._client.groups.by_group_id(group_id).owners.by_directory_object_id(obj_id).ref.delete() + def main(): AzureRMADGroup() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adgroup_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adgroup_info.py index 37bc1febb..3525bdf1b 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adgroup_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adgroup_info.py @@ -1,10 +1,11 @@ #!/usr/bin/python # -# Copyright (c) 2021 Cole Neubauer, (@coleneubauer) +# Copyright (c) 2021 Cole Neubauer, (@coleneubauer), xuzhang3 (@xuzhang3) # # 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 DOCUMENTATION = ''' @@ -14,11 +15,6 @@ short_description: Get Azure Active Directory group info description: - Get Azure Active Directory group info. options: - tenant: - description: - - The tenant ID. - type: str - required: True object_id: description: - The object id for the ad group. @@ -70,53 +66,46 @@ extends_documentation_fragment: - azure.azcollection.azure author: - Cole Neubauer(@coleneubauer) + - Xu Zhang(@xuzhang) ''' EXAMPLES = ''' - name: Return a specific group using object_id azure_rm_adgroup_info: object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - name: Return a specific group using object_id and return the owners of the group azure_rm_adgroup_info: object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx return_owners: true - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - name: Return a specific group using object_id and return the owners and members of the group azure_rm_adgroup_info: object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx return_owners: true return_group_members: true - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - name: Return a specific group using object_id and return the groups the group is a member of azure_rm_adgroup_info: object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx return_member_groups: true - tenant: "{{ tenant_id }}" - name: Return a specific group using object_id and check an ID for membership azure_rm_adgroup_info: object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx check_membership: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - name: Return a specific group using displayName for attribute_name azure_rm_adgroup_info: attribute_name: "displayName" attribute_value: "Display-Name-Of-AD-Group" - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - name: Return groups matching odata_filter azure_rm_adgroup_info: odata_filter: "mailNickname eq 'Mail-Nickname-Of-AD-Group'" - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - name: Return all groups azure_rm_adgroup_info: - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx all: true ''' @@ -167,13 +156,23 @@ group_members: - The members of the group. returned: always type: list +description: + description: + - An optional description for the group. + type: str + returned: always + sample: 'fortest' ''' from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBase try: - from azure.graphrbac.models import GraphErrorException - from azure.graphrbac.models import CheckGroupMembershipParameters + import asyncio + from msgraph.generated.groups.groups_request_builder import GroupsRequestBuilder + from msgraph.generated.groups.item.transitive_members.transitive_members_request_builder import \ + TransitiveMembersRequestBuilder + from msgraph.generated.groups.item.get_member_groups.get_member_groups_post_request_body import \ + GetMemberGroupsPostRequestBody except ImportError: # This is handled in azure_rm_common pass @@ -192,10 +191,8 @@ class AzureRMADGroupInfo(AzureRMModuleBase): return_group_members=dict(type='bool', default=False), return_member_groups=dict(type='bool', default=False), all=dict(type='bool', default=False), - tenant=dict(type='str', required=True), ) - self.tenant = None self.object_id = None self.attribute_name = None self.attribute_value = None @@ -207,6 +204,7 @@ class AzureRMADGroupInfo(AzureRMModuleBase): self.all = False self.results = dict(changed=False) + self._client = None mutually_exclusive = [['odata_filter', 'attribute_name', 'object_id', 'all']] required_together = [['attribute_name', 'attribute_value']] @@ -228,20 +226,19 @@ class AzureRMADGroupInfo(AzureRMModuleBase): ad_groups = [] try: - client = self.get_graphrbac_client(self.tenant) + self._client = self.get_msgraph_client() if self.object_id is not None: - ad_groups = [client.groups.get(self.object_id)] + ad_groups = [asyncio.get_event_loop().run_until_complete(self.get_group(self.object_id))] elif self.attribute_name is not None and self.attribute_value is not None: - ad_groups = list(client.groups.list(filter="{0} eq '{1}'".format(self.attribute_name, self.attribute_value))) + ad_groups = asyncio.get_event_loop().run_until_complete( + self.get_group_list(filter="{0} eq '{1}'".format(self.attribute_name, self.attribute_value))) elif self.odata_filter is not None: # run a filter based on user input - ad_groups = list(client.groups.list(filter=self.odata_filter)) + ad_groups = asyncio.get_event_loop().run_until_complete(self.get_group_list(filter=self.odata_filter)) elif self.all: - ad_groups = list(client.groups.list()) - - self.results['ad_groups'] = [self.set_results(group, client) for group in ad_groups] - - except GraphErrorException as e: + ad_groups = asyncio.get_event_loop().run_until_complete(self.get_group_list()) + self.results['ad_groups'] = [self.set_results(group) for group in ad_groups] + except Exception as e: self.fail("failed to get ad group info {0}".format(str(e))) return self.results @@ -256,24 +253,25 @@ class AzureRMADGroupInfo(AzureRMModuleBase): def serviceprincipal_to_dict(self, object): return dict( app_id=object.app_id, - object_id=object.object_id, + object_id=object.id, app_display_name=object.display_name, app_role_assignment_required=object.app_role_assignment_required ) def group_to_dict(self, object): return dict( - object_id=object.object_id, + object_id=object.id, display_name=object.display_name, mail_nickname=object.mail_nickname, mail_enabled=object.mail_enabled, security_enabled=object.security_enabled, - mail=object.mail + mail=object.mail, + description=object.description ) def user_to_dict(self, object): return dict( - object_id=object.object_id, + object_id=object.id, display_name=object.display_name, user_principal_name=object.user_principal_name, mail_nickname=object.mail_nickname, @@ -283,35 +281,94 @@ class AzureRMADGroupInfo(AzureRMModuleBase): ) def result_to_dict(self, object): - if object.object_type == "Group": + if object.odata_type == "#microsoft.graph.group": return self.group_to_dict(object) - elif object.object_type == "User": + elif object.odata_type == "#microsoft.graph.user": return self.user_to_dict(object) - elif object.object_type == "Application": + elif object.odata_type == "#microsoft.graph.application": return self.application_to_dict(object) - elif object.object_type == "ServicePrincipal": + elif object.odata_type == "#microsoft.graph.servicePrincipal": return self.serviceprincipal_to_dict(object) else: - return object.object_type + return object.odata_type - def set_results(self, object, client): + def set_results(self, object): results = self.group_to_dict(object) if results["object_id"] and self.return_owners: - results["group_owners"] = [self.result_to_dict(object) for object in list(client.groups.list_owners(results["object_id"]))] + ret = asyncio.get_event_loop().run_until_complete(self.get_group_owners(results["object_id"])) + results["group_owners"] = [self.result_to_dict(object) for object in ret.value] if results["object_id"] and self.return_group_members: - results["group_members"] = [self.result_to_dict(object) for object in list(client.groups.get_group_members(results["object_id"]))] + ret = asyncio.get_event_loop().run_until_complete(self.get_group_members(results["object_id"])) + results["group_members"] = [self.result_to_dict(object) for object in ret.value] if results["object_id"] and self.return_member_groups: - results["member_groups"] = [self.result_to_dict(object) for object in list(client.groups.get_member_groups(results["object_id"], False))] + ret = asyncio.get_event_loop().run_until_complete(self.get_member_groups(results["object_id"])) + results["member_groups"] = [self.result_to_dict(object) for object in list(ret.value)] if results["object_id"] and self.check_membership: - results["is_member_of"] = client.groups.is_member_of( - CheckGroupMembershipParameters(group_id=results["object_id"], member_id=self.check_membership)).value + filter = "id eq '{0}' ".format(self.check_membership) + ret = asyncio.get_event_loop().run_until_complete(self.get_group_members(results["object_id"], filter)) + results["is_member_of"] = True if ret.value and len(ret.value) != 0 else False return results + async def get_group(self, group_id): + return await self._client.groups.by_group_id(group_id).get() + + async def get_group_list(self, filter=None): + kwargs = {} + if filter: + request_configuration = GroupsRequestBuilder.GroupsRequestBuilderGetRequestConfiguration( + query_parameters=GroupsRequestBuilder.GroupsRequestBuilderGetQueryParameters( + count=True, + filter=filter, + ), + ) + kwargs["request_configuration"] = request_configuration + + groups = [] + # paginated response can be quite large + response = await self._client.groups.get(**kwargs) + if response: + groups += response.value + while response is not None and response.odata_next_link is not None: + response = await self._client.groups.with_url(response.odata_next_link).get(**kwargs) + if response: + groups += response.value + + return groups + + async def get_group_owners(self, group_id): + request_configuration = GroupsRequestBuilder.GroupsRequestBuilderGetRequestConfiguration( + query_parameters=GroupsRequestBuilder.GroupsRequestBuilderGetQueryParameters( + count=True, + select=['id', 'displayName', 'userPrincipalName', 'mailNickname', 'mail', 'accountEnabled', 'userType', + 'appId', 'appRoleAssignmentRequired'] + + ), + ) + return await self._client.groups.by_group_id(group_id).owners.get(request_configuration=request_configuration) + + async def get_group_members(self, group_id, filters=None): + request_configuration = TransitiveMembersRequestBuilder.TransitiveMembersRequestBuilderGetRequestConfiguration( + query_parameters=TransitiveMembersRequestBuilder.TransitiveMembersRequestBuilderGetQueryParameters( + count=True, + select=['id', 'displayName', 'userPrincipalName', 'mailNickname', 'mail', 'accountEnabled', 'userType', + 'appId', 'appRoleAssignmentRequired'] + + ), + ) + if filters: + request_configuration.query_parameters.filter = filters + return await self._client.groups.by_group_id(group_id).transitive_members.get( + request_configuration=request_configuration) + + async def get_member_groups(self, obj_id): + request_body = GetMemberGroupsPostRequestBody(security_enabled_only=False) + return await self._client.groups.by_group_id(obj_id).get_member_groups.post(body=request_body) + def main(): AzureRMADGroupInfo() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adpassword.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adpassword.py index 587d842b5..c6a634db9 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adpassword.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adpassword.py @@ -32,21 +32,17 @@ options: key_id: description: - The password key ID. + - It isn't supported anymore in the create operation. See the Azure documentation for more information + U(https://learn.microsoft.com/en-us/graph/api/application-addpassword?view=graph-rest-1.0&tabs=http#request-body). type: str - tenant: - description: - - The tenant ID. - type: str - required: True end_date: description: - Date or datemtime after which credentials expire. - Default value is one year after current time. type: str - value: + display_name: description: - - The application password value. - - Length greater than 18 characters. + - The friendly name of the application password. type: str app_object_id: description: @@ -77,8 +73,7 @@ EXAMPLES = ''' azure_rm_adpassword: app_id: "{{ app_id }}" state: present - value: "$abc12345678" - tenant: "{{ tenant_id }}" + display_name: "Password friendly name" ''' RETURN = ''' @@ -102,17 +97,26 @@ start_date: type: str returned: always sample: "2020-06-28T06:00:32.637070+00:00" - +secret_text: + description: + - The application password value. + - API only returns the application password value at creation. + type: str + returned: created + sample: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ''' from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase -import uuid import datetime try: - from azure.graphrbac.models import GraphErrorException - from azure.graphrbac.models import PasswordCredential - from azure.graphrbac.models import ApplicationUpdateParameters + import asyncio + from msgraph.generated.models.password_credential import PasswordCredential + from msgraph.generated.applications.item.add_password.add_password_post_request_body import \ + AddPasswordPostRequestBody + from msgraph.generated.applications.item.remove_password.remove_password_post_request_body import \ + RemovePasswordPostRequestBody + from msgraph.generated.applications.applications_request_builder import ApplicationsRequestBuilder from dateutil.relativedelta import relativedelta except ImportError: # This is handled in azure_rm_common @@ -127,19 +131,17 @@ class AzureRMADPassword(AzureRMModuleBase): service_principal_object_id=dict(type='str'), app_object_id=dict(type='str'), key_id=dict(type='str'), - tenant=dict(type='str', required=True), - value=dict(type='str'), + display_name=dict(type='str'), end_date=dict(type='str'), state=dict(type='str', default='present', choices=['present', 'absent']), ) self.state = None - self.tenant = None self.app_id = None self.service_principal_object_id = None self.app_object_id = None self.key_id = None - self.value = None + self.display_name = None self.end_date = None self.results = dict(changed=False) @@ -154,13 +156,13 @@ class AzureRMADPassword(AzureRMModuleBase): for key in list(self.module_arg_spec.keys()): setattr(self, key, kwargs[key]) - self.client = self.get_graphrbac_client(self.tenant) + self._client = self.get_msgraph_client() self.resolve_app_obj_id() passwords = self.get_all_passwords() if self.state == 'present': if self.key_id and self.key_exists(passwords): - self.update(passwords) + self.update_password(passwords) else: self.create_password(passwords) else: @@ -173,7 +175,7 @@ class AzureRMADPassword(AzureRMModuleBase): def key_exists(self, old_passwords): for pd in old_passwords: - if pd.key_id == self.key_id: + if str(pd.key_id) == self.key_id: return True return False @@ -183,27 +185,31 @@ class AzureRMADPassword(AzureRMModuleBase): return elif self.app_id or self.service_principal_object_id: if not self.app_id: - sp = self.client.service_principals.get(self.service_principal_object_id) + sp = asyncio.get_event_loop().run_until_complete(self.get_service_principal()) self.app_id = sp.app_id if not self.app_id: - self.fail("can't resolve app via service principal object id {0}".format(self.service_principal_object_id)) + self.fail("can't resolve app via service principal object id {0}".format( + self.service_principal_object_id)) - result = list(self.client.applications.list(filter="appId eq '{0}'".format(self.app_id))) + apps = asyncio.get_event_loop().run_until_complete(self.get_applications()) + result = list(apps.value) if result: - self.app_object_id = result[0].object_id + self.app_object_id = result[0].id else: self.fail("can't resolve app via app id {0}".format(self.app_id)) else: self.fail("one of the [app_id, app_object_id, service_principal_id] must be set") - except GraphErrorException as ge: + except Exception as ge: self.fail("error in resolve app_object_id {0}".format(str(ge))) def get_all_passwords(self): try: - return list(self.client.applications.list_password_credentials(self.app_object_id)) - except GraphErrorException as ge: + application = asyncio.get_event_loop().run_until_complete(self.get_application()) + passwordCredentials = application.password_credentials + return passwordCredentials + except Exception as ge: self.fail("failed to fetch passwords for app {0}: {1}".format(self.app_object_id, str(ge))) def delete_all_passwords(self, old_passwords): @@ -212,9 +218,10 @@ class AzureRMADPassword(AzureRMModuleBase): self.results['changed'] = False return try: - self.client.applications.patch(self.app_object_id, ApplicationUpdateParameters(password_credentials=[])) + for pd in old_passwords: + asyncio.get_event_loop().run_until_complete(self.remove_password(pd.key_id)) self.results['changed'] = True - except GraphErrorException as ge: + except Exception as ge: self.fail("fail to purge all passwords for app: {0} - {1}".format(self.app_object_id, str(ge))) def delete_password(self, old_passwords): @@ -225,46 +232,38 @@ class AzureRMADPassword(AzureRMModuleBase): num_of_passwords_before_delete = len(old_passwords) for pd in old_passwords: - if pd.key_id == self.key_id: - old_passwords.remove(pd) + if str(pd.key_id) == self.key_id: + try: + asyncio.get_event_loop().run_until_complete(self.remove_password(pd.key_id)) + + num_of_passwords_after_delete = len(self.get_all_passwords()) + if num_of_passwords_after_delete != num_of_passwords_before_delete: + self.results['changed'] = True + except Exception as ge: + self.fail("failed to delete password with key id {0} - {1}".format(self.app_id, str(ge))) break - try: - self.client.applications.patch(self.app_object_id, ApplicationUpdateParameters(password_credentials=old_passwords)) - num_of_passwords_after_delete = len(self.get_all_passwords()) - if num_of_passwords_after_delete != num_of_passwords_before_delete: - self.results['changed'] = True - - except GraphErrorException as ge: - self.fail("failed to delete password with key id {0} - {1}".format(self.app_id, str(ge))) def create_password(self, old_passwords): - - def gen_guid(): - return uuid.uuid4() - - if self.value is None: - self.fail("when creating a new password, module parameter value can't be None") - start_date = datetime.datetime.now(datetime.timezone.utc) end_date = self.end_date or start_date + relativedelta(years=1) - value = self.value - key_id = self.key_id or str(gen_guid()) - - new_password = PasswordCredential(start_date=start_date, end_date=end_date, key_id=key_id, - value=value, custom_key_identifier=None) - old_passwords.append(new_password) + display_name = self.display_name + num_of_passwords_before_add = len(old_passwords) try: - client = self.get_graphrbac_client(self.tenant) - app_patch_parameters = ApplicationUpdateParameters(password_credentials=old_passwords) - client.applications.patch(self.app_object_id, app_patch_parameters) - - new_passwords = self.get_all_passwords() - for pd in new_passwords: - if pd.key_id == key_id: - self.results['changed'] = True - self.results.update(self.to_dict(pd)) - except GraphErrorException as ge: + request_body = AddPasswordPostRequestBody( + password_credential=PasswordCredential( + start_date_time=start_date, + end_date_time=end_date, + display_name=display_name + ), + ) + pd = asyncio.get_event_loop().run_until_complete(self.add_password(request_body)) + + num_of_passwords_after_add = len(self.get_all_passwords()) + if num_of_passwords_after_add != num_of_passwords_before_add: + self.results['changed'] = True + self.results.update(self.to_dict(pd)) + except Exception as ge: self.fail("failed to create new password: {0}".format(str(ge))) def update_password(self, old_passwords): @@ -272,10 +271,36 @@ class AzureRMADPassword(AzureRMModuleBase): def to_dict(self, pd): return dict( - end_date=pd.end_date, - start_date=pd.start_date, - key_id=pd.key_id + end_date=str(pd.end_date_time), + start_date=str(pd.start_date_time), + key_id=str(pd.key_id), + secret_text=str(pd.secret_text) + ) + + async def get_service_principal(self): + return await self._client.service_principals.by_service_principal_id(self.service_principal_object_id).get() + + async def get_applications(self): + request_configuration = ApplicationsRequestBuilder.ApplicationsRequestBuilderGetRequestConfiguration( + query_parameters=ApplicationsRequestBuilder.ApplicationsRequestBuilderGetQueryParameters( + filter="appId eq '{0}'".format(self.app_id), + ), ) + return await self._client.applications.get(request_configuration=request_configuration) + + async def get_application(self): + return await self._client.applications.by_application_id(self.app_object_id).get() + + async def remove_password(self, key_id): + request_body = RemovePasswordPostRequestBody( + key_id=key_id, + ) + return await self._client.applications.by_application_id(self.app_object_id).remove_password.post( + body=request_body) + + async def add_password(self, request_body): + return await self._client.applications.by_application_id(self.app_object_id).add_password.post( + body=request_body) def main(): diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adpassword_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adpassword_info.py index 7c82b7b9f..229ef100d 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adpassword_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adpassword_info.py @@ -8,7 +8,6 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type - DOCUMENTATION = ''' module: azure_rm_adpassword_info @@ -32,11 +31,6 @@ options: description: - The password key ID. type: str - tenant: - description: - - The tenant ID. - type: str - required: True end_date: description: - Date or datemtime after which credentials expire. @@ -64,7 +58,6 @@ EXAMPLES = ''' - name: get ad password info azure_rm_adpassword_info: app_id: "{{ app_id }}" - tenant: "{{ tenant_id }}" key_id: "{{ key_id }}" ''' @@ -107,7 +100,8 @@ passwords: from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase try: - from azure.graphrbac.models import GraphErrorException + import asyncio + from msgraph.generated.applications.applications_request_builder import ApplicationsRequestBuilder except ImportError: # This is handled in azure_rm_common pass @@ -121,12 +115,10 @@ class AzureRMADPasswordInfo(AzureRMModuleBase): app_object_id=dict(type='str'), service_principal_object_id=dict(type='str'), key_id=dict(type='str'), - tenant=dict(type='str', required=True), value=dict(type='str'), end_date=dict(type='str'), ) - self.tenant = None self.app_id = None self.service_principal_object_id = None self.app_object_id = None @@ -147,12 +139,12 @@ class AzureRMADPasswordInfo(AzureRMModuleBase): for key in list(self.module_arg_spec.keys()): setattr(self, key, kwargs[key]) - self.client = self.get_graphrbac_client(self.tenant) + self._client = self.get_msgraph_client() self.resolve_app_obj_id() passwords = self.get_all_passwords() if self.key_id: - filtered = [pd for pd in passwords if pd.key_id == self.key_id] + filtered = [pd for pd in passwords if str(pd.key_id) == self.key_id] self.results['passwords'] = [self.to_dict(pd) for pd in filtered] else: self.results['passwords'] = [self.to_dict(pd) for pd in passwords] @@ -165,37 +157,55 @@ class AzureRMADPasswordInfo(AzureRMModuleBase): return elif self.app_id or self.service_principal_object_id: if not self.app_id: - sp = self.client.service_principals.get(self.service_principal_id) + sp = asyncio.get_event_loop().run_until_complete(self.get_service_principal()) self.app_id = sp.app_id if not self.app_id: - self.fail("can't resolve app via service principal object id {0}".format(self.service_principal_object_id)) + self.fail("can't resolve app via service principal object id {0}".format( + self.service_principal_object_id)) - result = list(self.client.applications.list(filter="appId eq '{0}'".format(self.app_id))) + apps = asyncio.get_event_loop().run_until_complete(self.get_applications()) + result = list(apps.value) if result: - self.app_object_id = result[0].object_id + self.app_object_id = result[0].id else: self.fail("can't resolve app via app id {0}".format(self.app_id)) else: - self.fail("one of the [app_id, app_object_id, service_principal_id] must be set") + self.fail("one of the [app_id, app_object_id, service_principal_object_id] must be set") - except GraphErrorException as ge: + except Exception as ge: self.fail("error in resolve app_object_id {0}".format(str(ge))) def get_all_passwords(self): try: - return list(self.client.applications.list_password_credentials(self.app_object_id)) - except GraphErrorException as ge: + application = asyncio.get_event_loop().run_until_complete(self.get_application()) + passwordCredentials = application.password_credentials + return passwordCredentials + except Exception as ge: self.fail("failed to fetch passwords for app {0}: {1}".format(self.app_object_id, str(ge))) def to_dict(self, pd): return dict( - end_date=pd.end_date, - start_date=pd.start_date, - key_id=pd.key_id, + end_date=pd.end_date_time, + start_date=pd.start_date_time, + key_id=str(pd.key_id), custom_key_identifier=str(pd.custom_key_identifier) ) + async def get_service_principal(self): + return await self._client.service_principals.by_service_principal_id(self.service_principal_object_id).get() + + async def get_applications(self): + request_configuration = ApplicationsRequestBuilder.ApplicationsRequestBuilderGetRequestConfiguration( + query_parameters=ApplicationsRequestBuilder.ApplicationsRequestBuilderGetQueryParameters( + filter="appId eq '{0}'".format(self.app_id), + ), + ) + return await self._client.applications.get(request_configuration=request_configuration) + + async def get_application(self): + return await self._client.applications.by_application_id(self.app_object_id).get() + def main(): AzureRMADPasswordInfo() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adserviceprincipal.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adserviceprincipal.py index a7d3b39fd..ef12d642d 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adserviceprincipal.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adserviceprincipal.py @@ -5,8 +5,8 @@ # 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 +__metaclass__ = type DOCUMENTATION = ''' --- @@ -25,11 +25,6 @@ options: - The application ID. type: str required: True - tenant: - description: - - The tenant ID. - type: str - required: True app_role_assignment_required: description: - Whether the Role of the Service Principal is set. @@ -57,7 +52,6 @@ EXAMPLES = ''' azure_rm_adserviceprincipal: app_id: "{{ app_id }}" state: present - tenant: "{{ tenant_id }}" ''' RETURN = ''' @@ -89,13 +83,15 @@ object_id: ''' from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBaseExt + try: - from azure.graphrbac.models import ServicePrincipalCreateParameters + from msgraph.generated.models.service_principal import ServicePrincipal except Exception: pass try: - from azure.graphrbac.models import GraphErrorException + import asyncio + from msgraph.generated.service_principals.service_principals_request_builder import ServicePrincipalsRequestBuilder except ImportError: # This is handled in azure_rm_common pass @@ -106,13 +102,11 @@ class AzureRMADServicePrincipal(AzureRMModuleBaseExt): self.module_arg_spec = dict( app_id=dict(type='str', required=True), - tenant=dict(type='str', required=True), state=dict(type='str', default='present', choices=['present', 'absent']), app_role_assignment_required=dict(type='bool') ) self.state = None - self.tenant = None self.app_id = None self.app_role_assignment_required = None self.object_id = None @@ -128,6 +122,8 @@ class AzureRMADServicePrincipal(AzureRMModuleBaseExt): for key in list(self.module_arg_spec.keys()): setattr(self, key, kwargs[key]) + self._client = self.get_msgraph_client() + response = self.get_resource() if response: @@ -146,46 +142,47 @@ class AzureRMADServicePrincipal(AzureRMModuleBaseExt): def create_resource(self): try: - client = self.get_graphrbac_client(self.tenant) - response = client.service_principals.create(ServicePrincipalCreateParameters(app_id=self.app_id, account_enabled=True)) + response = asyncio.get_event_loop().run_until_complete(self.create_service_principal()) self.results['changed'] = True self.results.update(self.to_dict(response)) return response - except GraphErrorException as ge: + except Exception as ge: self.fail("Error creating service principle, app id {0} - {1}".format(self.app_id, str(ge))) def update_resource(self, old_response): try: - client = self.get_graphrbac_client(self.tenant) - to_update = {} + request_body = ServicePrincipal( + app_role_assignment_required=None, + ) if self.app_role_assignment_required is not None: - to_update['app_role_assignment_required'] = self.app_role_assignment_required + request_body = ServicePrincipal( + app_role_assignment_required=self.app_role_assignment_required + ) - client.service_principals.update(old_response['object_id'], to_update) + asyncio.get_event_loop().run_until_complete(self.update_service_principal(old_response, request_body)) self.results['changed'] = True self.results.update(self.get_resource()) - except GraphErrorException as ge: + except Exception as ge: self.fail("Error updating the service principal app_id {0} - {1}".format(self.app_id, str(ge))) def delete_resource(self, response): try: - client = self.get_graphrbac_client(self.tenant) - client.service_principals.delete(response.get('object_id')) + asyncio.get_event_loop().run_until_complete(self.delete_service_principal(response)) self.results['changed'] = True return True - except GraphErrorException as ge: + except Exception as ge: self.fail("Error deleting service principal app_id {0} - {1}".format(self.app_id, str(ge))) def get_resource(self): try: - client = self.get_graphrbac_client(self.tenant) - result = list(client.service_principals.list(filter="servicePrincipalNames/any(c:c eq '{0}')".format(self.app_id))) + sps = asyncio.get_event_loop().run_until_complete(self.get_service_principals()) + result = list(sps.value) if not result: return False result = result[0] return self.to_dict(result) - except GraphErrorException as ge: + except Exception as ge: self.log("Did not find the graph instance instance {0} - {1}".format(self.app_id, str(ge))) return False @@ -198,11 +195,33 @@ class AzureRMADServicePrincipal(AzureRMModuleBaseExt): def to_dict(self, object): return dict( app_id=object.app_id, - object_id=object.object_id, + object_id=object.id, app_display_name=object.display_name, app_role_assignment_required=object.app_role_assignment_required ) + async def create_service_principal(self): + request_body = ServicePrincipal( + app_id=self.app_id, + account_enabled=True + ) + return await self._client.service_principals.post(body=request_body) + + async def update_service_principal(self, old_response, request_body): + return await self._client.service_principals.by_service_principal_id(old_response['object_id']).patch( + body=request_body) + + async def delete_service_principal(self, response): + return await self._client.service_principals.by_service_principal_id(response.get('object_id')).delete() + + async def get_service_principals(self): + request_configuration = ServicePrincipalsRequestBuilder.ServicePrincipalsRequestBuilderGetRequestConfiguration( + query_parameters=ServicePrincipalsRequestBuilder.ServicePrincipalsRequestBuilderGetQueryParameters( + filter="servicePrincipalNames/any(c:c eq '{0}')".format(self.app_id), + ) + ) + return await self._client.service_principals.get(request_configuration=request_configuration) + def main(): AzureRMADServicePrincipal() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adserviceprincipal_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adserviceprincipal_info.py index db27ccae8..d2acfac04 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adserviceprincipal_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_adserviceprincipal_info.py @@ -5,8 +5,8 @@ # 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 +__metaclass__ = type DOCUMENTATION = ''' module: azure_rm_adserviceprincipal_info @@ -23,11 +23,6 @@ options: description: - The application ID. type: str - tenant: - description: - - The tenant ID. - type: str - required: True object_id: description: - It's service principal's object ID. @@ -45,34 +40,43 @@ EXAMPLES = ''' - name: get ad sp info azure_rm_adserviceprincipal_info: app_id: "{{ app_id }}" - tenant: "{{ tenant_id }}" +- name: get all service principals + azure_rm_adserviceprincipal_info: ''' RETURN = ''' -app_display_name: - description: - - Object's display name or its prefix. - type: str - returned: always - sample: sp -app_id: +service_principals: description: - - The application ID. + - A list of service principals in the tenant. If app_id or object_id is set, the maximum length + of this list should be one. + type: list + elements: dict returned: always - type: str - sample: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -app_role_assignment_required: - description: - - Whether the Role of the Service Principal is set. - type: bool - returned: always - sample: false -object_id: - description: - - It's service principal's object ID. - returned: always - type: str - sample: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + contains: + app_display_name: + description: + - Object's display name or its prefix. + type: str + returned: always + sample: sp + app_id: + description: + - The application ID. + returned: always + type: str + sample: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + app_role_assignment_required: + description: + - Whether the Role of the Service Principal is set. + type: bool + returned: always + sample: false + object_id: + description: + - It's service principal's object ID. + returned: always + type: str + sample: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ''' @@ -80,7 +84,8 @@ object_id: from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBase try: - from azure.graphrbac.models import GraphErrorException + import asyncio + from msgraph.generated.service_principals.service_principals_request_builder import ServicePrincipalsRequestBuilder except ImportError: # This is handled in azure_rm_common pass @@ -92,10 +97,8 @@ class AzureRMADServicePrincipalInfo(AzureRMModuleBase): self.module_arg_spec = dict( app_id=dict(type='str'), object_id=dict(type='str'), - tenant=dict(type='str', required=True), ) - self.tenant = None self.app_id = None self.object_id = None self.results = dict(changed=False) @@ -110,17 +113,18 @@ class AzureRMADServicePrincipalInfo(AzureRMModuleBase): for key in list(self.module_arg_spec.keys()): setattr(self, key, kwargs[key]) + self._client = self.get_msgraph_client() + service_principals = [] try: - client = self.get_graphrbac_client(self.tenant) if self.object_id is None: - service_principals = list(client.service_principals.list(filter="servicePrincipalNames/any(c:c eq '{0}')".format(self.app_id))) + service_principals = asyncio.get_event_loop().run_until_complete(self.get_service_principals()) else: - service_principals = [client.service_principals.get(self.object_id)] + service_principals = [asyncio.get_event_loop().run_until_complete(self.get_service_principal())] self.results['service_principals'] = [self.to_dict(sp) for sp in service_principals] - except GraphErrorException as ge: + except Exception as ge: self.fail("failed to get service principal info {0}".format(str(ge))) return self.results @@ -128,11 +132,32 @@ class AzureRMADServicePrincipalInfo(AzureRMModuleBase): def to_dict(self, object): return dict( app_id=object.app_id, - object_id=object.object_id, + object_id=object.id, app_display_name=object.display_name, app_role_assignment_required=object.app_role_assignment_required ) + async def get_service_principal(self): + return await self._client.service_principals.by_service_principal_id(self.object_id).get() + + async def get_service_principals(self): + kwargs = {} + if self.app_id is not None: + request_configuration = ServicePrincipalsRequestBuilder.ServicePrincipalsRequestBuilderGetRequestConfiguration( + query_parameters=ServicePrincipalsRequestBuilder.ServicePrincipalsRequestBuilderGetQueryParameters( + filter="servicePrincipalNames/any(c:c eq '{0}')".format(self.app_id)) + ) + kwargs['request_configuration'] = request_configuration + service_principals = [] + response = await self._client.service_principals.get(**kwargs) + if response: + service_principals += response.value + while response is not None and response.odata_next_link is not None: + response = await self._client.service_principals.with_url(response.odata_next_link).get(**kwargs) + if response: + service_principals += response.value + return service_principals + def main(): AzureRMADServicePrincipalInfo() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aduser.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aduser.py index b41c5ae7e..1e0a238c0 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aduser.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aduser.py @@ -5,6 +5,7 @@ # 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 DOCUMENTATION = ''' @@ -18,11 +19,6 @@ description: - Create, delete, and update an Azure Active Directory user. options: - tenant: - description: - - The tenant ID. - type: str - required: True state: description: - State of the ad user. Use C(present) to create or update an ad user and C(absent) to delete an ad user. @@ -57,11 +53,13 @@ options: - The surname for the user. - Used when either creating or updating a user account. type: str - immutable_id: + on_premises_immutable_id: description: - - The immutable_id of the user. + - The on_premises_immutable_id of the user. - Used when either creating or updating a user account. type: str + aliases: + - immutable_id mail: description: - The primary email address of the user. @@ -114,6 +112,13 @@ options: - Filter that can be used to specify a user to update or delete. - Mutually exclusive with I(object_id), I(attribute_name), and I(user_principal_name). type: str + company_name: + description: + - The name of the company that the user is associated with. + - This property can be useful for describing the company that an external user comes from. + - The maximum length is 64 characters.Returned only on $select. + - Supports $filter (eq, ne, not, ge, le, in, startsWith, and eq on null values). + type: str extends_documentation_fragment: - azure.azcollection.azure @@ -126,30 +131,28 @@ EXAMPLES = ''' - name: Create user azure_rm_aduser: user_principal_name: "{{ user_id }}" - tenant: "{{ tenant_id }}" state: "present" account_enabled: "True" display_name: "Test_{{ user_principal_name }}_Display_Name" password_profile: "password" mail_nickname: "Test_{{ user_principal_name }}_mail_nickname" - immutable_id: "{{ object_id }}" + on_premises_immutable_id: "{{ object_id }}" given_name: "First" surname: "Last" user_type: "Member" usage_location: "US" mail: "{{ user_principal_name }}@contoso.com" + company_name: 'Test Company' - name: Update user with new value for account_enabled azure_rm_aduser: user_principal_name: "{{ user_id }}" - tenant: "{{ tenant_id }}" state: "present" account_enabled: "False" - name: Delete user azure_rm_aduser: user_principal_name: "{{ user_id }}" - tenant: "{{ tenant_id }}" state: "absent" ''' @@ -196,15 +199,21 @@ user_type: returned: always type: str sample: Member +company_name: + description: + - The name of the company that the user is associated with. + type: str + returned: always + sample: 'Test Company' ''' from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBase try: - from azure.graphrbac.models import UserUpdateParameters - from azure.graphrbac.models import UserCreateParameters - from azure.graphrbac.models import PasswordProfile - from azure.graphrbac.models import GraphErrorException + import asyncio + from msgraph.generated.models.password_profile import PasswordProfile + from msgraph.generated.models.user import User + from msgraph.generated.users.users_request_builder import UsersRequestBuilder except ImportError: # This is handled in azure_rm_common pass @@ -224,16 +233,15 @@ class AzureRMADUser(AzureRMModuleBase): display_name=dict(type='str'), password_profile=dict(type='str', no_log=True), mail_nickname=dict(type='str'), - immutable_id=dict(type='str'), + on_premises_immutable_id=dict(type='str', aliases=['immutable_id']), usage_location=dict(type='str'), given_name=dict(type='str'), surname=dict(type='str'), user_type=dict(type='str'), mail=dict(type='str'), - tenant=dict(type='str', required=True), + company_name=dict(type='str') ) - self.tenant = None self.user_principal_name = None self.state = None self.object_id = None @@ -244,12 +252,13 @@ class AzureRMADUser(AzureRMModuleBase): self.display_name = None self.password_profile = None self.mail_nickname = None - self.immutable_id = None + self.on_premises_immutable_id = None self.usage_location = None self.given_name = None self.surname = None self.user_type = None self.mail = None + self.company_name = None self.log_path = None self.log_mode = None @@ -273,9 +282,9 @@ class AzureRMADUser(AzureRMModuleBase): setattr(self, key, kwargs[key]) try: - client = self.get_graphrbac_client(self.tenant) + self._client = self.get_msgraph_client() - ad_user = self.get_exisiting_user(client) + ad_user = self.get_exisiting_user() if self.state == 'present': @@ -284,11 +293,13 @@ class AzureRMADUser(AzureRMModuleBase): password = None if self.password_profile: - password = PasswordProfile(password=self.password_profile) + password = PasswordProfile( + password=self.password_profile, + ) should_update = False - if self.immutable_id and ad_user.immutable_id != self.immutable_id: + if self.on_premises_immutable_id and ad_user.on_premises_immutable_id != self.on_premises_immutable_id: should_update = True if should_update or self.usage_location and ad_user.usage_location != self.usage_location: should_update = True @@ -308,84 +319,73 @@ class AzureRMADUser(AzureRMModuleBase): should_update = True if should_update or self.mail_nickname and ad_user.mail_nickname != self.mail_nickname: should_update = True + if should_update or self.company_name and ad_user.company_name != self.company_name: + should_update = True if should_update: - parameters = UserUpdateParameters(immutable_id=self.immutable_id, - usage_location=self.usage_location, - given_name=self.given_name, - surname=self.surname, - user_type=self.user_type, - account_enabled=self.account_enabled, - display_name=self.display_name, - password_profile=password, - user_principal_name=self.user_principal_name, - mail_nickname=self.mail_nickname) - - client.users.update(upn_or_object_id=ad_user.object_id, parameters=parameters) + asyncio.get_event_loop().run_until_complete(self.update_user(ad_user, password)) self.results['changed'] = True # Get the updated versions of the users to return # the update method, has no return value so it needs to be explicitely returned in a call - ad_user = self.get_exisiting_user(client) + ad_user = self.get_exisiting_user() else: self.results['changed'] = False else: # Create, changed - password = PasswordProfile(password=self.password_profile) - parameters = UserCreateParameters(account_enabled=self.account_enabled, - display_name=self.display_name, - password_profile=password, - user_principal_name=self.user_principal_name, - mail_nickname=self.mail_nickname, - immutable_id=self.immutable_id, - usage_location=self.usage_location, - given_name=self.given_name, - surname=self.surname, - user_type=self.user_type, - mail=self.mail) - ad_user = client.users.create(parameters=parameters) + asyncio.get_event_loop().run_until_complete(self.create_user()) self.results['changed'] = True + ad_user = self.get_exisiting_user() self.results['ad_user'] = self.to_dict(ad_user) elif self.state == 'absent': if ad_user: # Delete, changed - client.users.delete(ad_user.object_id) + asyncio.get_event_loop().run_until_complete(self.delete_user(ad_user)) self.results['changed'] = True else: # Do nothing unchanged self.results['changed'] = False - except GraphErrorException as e: + except Exception as e: self.fail("failed to get ad user info {0}".format(str(e))) return self.results - def get_exisiting_user(self, client): + def get_exisiting_user(self): ad_user = None try: if self.user_principal_name is not None: - ad_user = client.users.get(self.user_principal_name) + ad_user = asyncio.get_event_loop().run_until_complete(self.get_user(self.user_principal_name)) elif self.object_id is not None: - ad_user = client.users.get(self.object_id) + ad_user = asyncio.get_event_loop().run_until_complete(self.get_user(self.object_id)) elif self.attribute_name is not None and self.attribute_value is not None: try: - ad_user = list(client.users.list(filter="{0} eq '{1}'".format(self.attribute_name, self.attribute_value)))[0] - except GraphErrorException as e: + users = asyncio.get_event_loop().run_until_complete( + self.get_users_by_filter("{0} eq '{1}'".format(self.attribute_name, self.attribute_value))) + ad_users = list(users.value) + ad_user = ad_users[0] + except Exception as e: # the type doesn't get more specific. Could check the error message but no guarantees that message doesn't change in the future # more stable to try again assuming the first error came from the attribute being a list try: - ad_user = list(client.users.list(filter="{0}/any(c:c eq '{1}')".format(self.attribute_name, self.attribute_value)))[0] - except GraphErrorException as sub_e: + users = asyncio.get_event_loop().run_until_complete(self.get_users_by_filter( + "{0}/any(c:c eq '{1}')".format(self.attribute_name, self.attribute_value))) + ad_users = list(users.value) + ad_user = ad_users[0] + except Exception as sub_e: raise elif self.odata_filter is not None: # run a filter based on user input to return based on any given attribute/query - ad_user = list(client.users.list(filter=self.odata_filter))[0] - except GraphErrorException as e: + users = asyncio.get_event_loop().run_until_complete(self.get_users_by_filter(self.odata_filter)) + ad_users = list(users.value) + ad_user = ad_users[0] + except Exception as e: # User was not found err_msg = str(e) - if err_msg == "Resource '{0}' does not exist or one of its queried reference-property objects are not present.".format(self.user_principal_name): + if "Resource '{0}' does not exist or one of its queried reference-property objects are not present.".format( + self.user_principal_name) in err_msg: ad_user = None else: raise @@ -393,13 +393,75 @@ class AzureRMADUser(AzureRMModuleBase): def to_dict(self, object): return dict( - object_id=object.object_id, + object_id=object.id, display_name=object.display_name, user_principal_name=object.user_principal_name, mail_nickname=object.mail_nickname, mail=object.mail, account_enabled=object.account_enabled, - user_type=object.user_type + user_type=object.user_type, + company_name=object.company_name + ) + + async def update_user(self, ad_user, password): + request_body = User( + on_premises_immutable_id=self.on_premises_immutable_id, + usage_location=self.usage_location, + given_name=self.given_name, + surname=self.surname, + user_type=self.user_type, + account_enabled=self.account_enabled, + display_name=self.display_name, + password_profile=password, + user_principal_name=self.user_principal_name, + mail_nickname=self.mail_nickname, + company_name=self.company_name + ) + return await self._client.users.by_user_id(ad_user.id).patch(body=request_body) + + async def create_user(self): + password = PasswordProfile( + password=self.password_profile + ) + request_body = User( + account_enabled=self.account_enabled, + display_name=self.display_name, + password_profile=password, + user_principal_name=self.user_principal_name, + mail_nickname=self.mail_nickname, + on_premises_immutable_id=self.on_premises_immutable_id, + usage_location=self.usage_location, + given_name=self.given_name, + surname=self.surname, + user_type=self.user_type, + mail=self.mail, + company_name=self.company_name + ) + return await self._client.users.post(body=request_body) + + async def delete_user(self, ad_user): + return await self._client.users.by_user_id(ad_user.id).delete() + + async def get_user(self, object): + request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( + query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( + select=["accountEnabled", "displayName", "mail", "mailNickname", "id", "userPrincipalName", "userType", + "onPremisesImmutableId", "usageLocation", "givenName", "surname", "companyName"] + ), + ) + return await self._client.users.by_user_id(object).get(request_configuration=request_configuration) + + async def get_users_by_filter(self, filter): + return await self._client.users.get( + request_configuration=UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( + query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( + filter=filter, + select=["accountEnabled", "displayName", "mail", "mailNickname", "id", "userPrincipalName", + "userType", "onPremisesImmutableId", "usageLocation", "givenName", "surname", "companyName"], + count=True + ), + headers={'ConsistencyLevel': "eventual", } + ) ) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aduser_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aduser_info.py index 85460e741..98c30be57 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aduser_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aduser_info.py @@ -5,6 +5,7 @@ # 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 DOCUMENTATION = ''' @@ -18,11 +19,6 @@ description: - Get Azure Active Directory user info. options: - tenant: - description: - - The tenant ID. - type: str - required: True object_id: description: - The object id for the user. @@ -74,34 +70,28 @@ EXAMPLES = ''' - name: Using user_principal_name azure.azcollection.azure_rm_aduser_info: user_principal_name: user@contoso.com - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - name: Using object_id azure.azcollection.azure_rm_aduser_info: object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - name: Using attribute mailNickname - not a collection azure.azcollection.azure_rm_aduser_info: attribute_name: mailNickname attribute_value: users_mailNickname - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - name: Using attribute proxyAddresses - a collection azure.azcollection.azure_rm_aduser_info: attribute_name: proxyAddresses attribute_value: SMTP:user@contoso.com - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - name: Using Filter mailNickname azure.azcollection.azure_rm_aduser_info: odata_filter: mailNickname eq 'user@contoso.com' - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - name: Using Filter proxyAddresses azure.azcollection.azure_rm_aduser_info: odata_filter: proxyAddresses/any(c:c eq 'SMTP:user@contoso.com') - tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ''' RETURN = ''' @@ -147,12 +137,19 @@ user_type: returned: always type: str sample: Member +company_name: + description: + - The name of the company that the user is associated with. + type: str + returned: always + sample: "Test Company" ''' from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBase try: - from azure.graphrbac.models import GraphErrorException + import asyncio + from msgraph.generated.users.users_request_builder import UsersRequestBuilder except ImportError: # This is handled in azure_rm_common pass @@ -168,10 +165,8 @@ class AzureRMADUserInfo(AzureRMModuleBase): attribute_value=dict(type='str'), odata_filter=dict(type='str'), all=dict(type='bool'), - tenant=dict(type='str', required=True), ) - self.tenant = None self.user_principal_name = None self.object_id = None self.attribute_name = None @@ -203,44 +198,88 @@ class AzureRMADUserInfo(AzureRMModuleBase): ad_users = [] try: - client = self.get_graphrbac_client(self.tenant) + self._client = self.get_msgraph_client() if self.user_principal_name is not None: - ad_users = [client.users.get(self.user_principal_name)] + ad_users = [asyncio.get_event_loop().run_until_complete(self.get_user(self.user_principal_name))] elif self.object_id is not None: - ad_users = [client.users.get(self.object_id)] + ad_users = [asyncio.get_event_loop().run_until_complete(self.get_user(self.object_id))] elif self.attribute_name is not None and self.attribute_value is not None: try: - ad_users = list(client.users.list(filter="{0} eq '{1}'".format(self.attribute_name, self.attribute_value))) - except GraphErrorException as e: + users = asyncio.get_event_loop().run_until_complete( + self.get_users_by_filter("{0} eq '{1}'".format(self.attribute_name, self.attribute_value))) + ad_users = list(users.value) + except Exception as e: # the type doesn't get more specific. Could check the error message but no guarantees that message doesn't change in the future # more stable to try again assuming the first error came from the attribute being a list try: - ad_users = list(client.users.list(filter="{0}/any(c:c eq '{1}')".format(self.attribute_name, self.attribute_value))) - except GraphErrorException as sub_e: + users = asyncio.get_event_loop().run_until_complete(self.get_users_by_filter( + "{0}/any(c:c eq '{1}')".format(self.attribute_name, self.attribute_value))) + ad_users = list(users.value) + except Exception as sub_e: raise elif self.odata_filter is not None: # run a filter based on user input to return based on any given attribute/query - ad_users = list(client.users.list(filter=self.odata_filter)) + users = asyncio.get_event_loop().run_until_complete(self.get_users_by_filter(self.odata_filter)) + ad_users = list(users.value) elif self.all: - ad_users = list(client.users.list()) + # this returns as a list, since we parse multiple pages + ad_users = asyncio.get_event_loop().run_until_complete(self.get_users()) self.results['ad_users'] = [self.to_dict(user) for user in ad_users] - except GraphErrorException as e: + except Exception as e: self.fail("failed to get ad user info {0}".format(str(e))) return self.results def to_dict(self, object): return dict( - object_id=object.object_id, + object_id=object.id, display_name=object.display_name, user_principal_name=object.user_principal_name, mail_nickname=object.mail_nickname, mail=object.mail, account_enabled=object.account_enabled, - user_type=object.user_type + user_type=object.user_type, + company_name=object.company_name + ) + + async def get_user(self, object): + request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( + query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( + select=["accountEnabled", "displayName", "mail", "mailNickname", "id", "userPrincipalName", "userType", "companyName"] + ), + ) + return await self._client.users.by_user_id(object).get(request_configuration=request_configuration) + + async def get_users(self): + request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( + query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( + select=["accountEnabled", "displayName", "mail", "mailNickname", "id", "userPrincipalName", "userType", "companyName"] + ), ) + users = [] + # paginated response can be quite large + response = await self._client.users.get(request_configuration=request_configuration) + if response: + users += response.value + while response is not None and response.odata_next_link is not None: + response = await self._client.users.with_url(response.odata_next_link).get(request_configuration=request_configuration) + if response: + users += response.value + + return users + + async def get_users_by_filter(self, filter): + return await self._client.users.get( + request_configuration=UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( + query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( + filter=filter, + select=["accountEnabled", "displayName", "mail", "mailNickname", "id", "userPrincipalName", + "userType", "companyName"], + count=True + ), + )) def main(): diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aks.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aks.py index bb034b48b..0fb5095fe 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aks.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aks.py @@ -284,7 +284,7 @@ options: type: str managed: description: - - Whether to enable manged AAD. + - Whether to enable managed AAD. type: bool default: false admin_group_object_ids: @@ -356,6 +356,57 @@ options: - Name of the resource group containing agent pool nodes. - Unable to update. type: str + pod_identity_profile: + description: + - Config pod identities in managed Kubernetes cluster. + type: dict + suboptions: + enabled: + description: + - Whether the pod identity addon is enabled. + type: bool + allow_network_plugin_kubenet: + description: + - Whether using Kubenet network plugin with AAD Pod Identity. + type: bool + user_assigned_identities: + description: + - The pod identities to use in the cluster. + type: list + elements: dict + suboptions: + name: + description: + - The name of the pod identity. + type: str + required: true + namespace: + description: + - The namespace of the pod identity. + type: str + required: true + binding_selector: + description: + - The binding selector to use for the AzureIdentityBinding resource. + type: str + identity: + description: + - The user assigned identity details. + type: dict + required: true + suboptions: + resource_id: + description: + - The resource ID of the user assigned identity. + type: str + object_id: + description: + - The object ID of the user assigned identity. + type: str + client_id: + description: + - The client ID of the user assigned identity. + type: str extends_documentation_fragment: - azure.azcollection.azure @@ -463,6 +514,45 @@ EXAMPLES = ''' type: VirtualMachineScaleSets enable_auto_scaling: false +- name: Create an AKS instance wit pod_identity_profile settings + azure_rm_aks: + name: "aks{{ rpfx }}" + resource_group: "{{ resource_group }}" + location: eastus + dns_prefix: "aks{{ rpfx }}" + kubernetes_version: "{{ versions.azure_aks_versions[0] }}" + service_principal: + client_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + client_secret: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + linux_profile: + admin_username: azureuser + ssh_key: ssh-rsa AAAAB3Ip6*************** + agent_pool_profiles: + - name: default + count: 1 + vm_size: Standard_B2s + type: VirtualMachineScaleSets + mode: System + node_labels: {"release":"stable"} + max_pods: 42 + availability_zones: + - 1 + - 2 + node_resource_group: "node{{ noderpfx }}" + enable_rbac: true + network_profile: + load_balancer_sku: standard + pod_identity_profile: + enabled: false + allow_network_plugin_kubenet: false + user_assigned_identities: + - name: fredtest + namespace: fredtest + binding_selector: test + identity: + client_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + - name: Remove a managed Azure Container Services (AKS) instance azure_rm_aks: name: myAKS @@ -490,7 +580,7 @@ state: changed: false dns_prefix: aks9860bdcd89 id: "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourcegroups/myResourceGroup/providers/Microsoft.ContainerService/managedClusters/aks9860bdc" - kube_config: "......" + kube_config: ["......"] kubernetes_version: 1.14.6 linux_profile: admin_username: azureuser @@ -500,13 +590,29 @@ state: provisioning_state: Succeeded service_principal_profile: client_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + pod_identity_profile: { + "allow_network_plugin_kubenet": false, + "user_assigned_identities": [ + { + "binding_selector": "test", + "identity": { + "client_id": xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, + "object_id": xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + }, + "name": "fredtest", + "namespace": "fredtest", + "provisioning_state": "Updating" + } + ] + } tags: {} type: Microsoft.ContainerService/ManagedClusters ''' -from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBaseExt try: from azure.core.exceptions import ResourceNotFoundError + from azure.core.exceptions import HttpResponseError except ImportError: # This is handled in azure_rm_common pass @@ -540,10 +646,19 @@ def create_aks_dict(aks): api_server_access_profile=create_api_server_access_profile_dict(aks.api_server_access_profile), addon=create_addon_dict(aks.addon_profiles), fqdn=aks.fqdn, - node_resource_group=aks.node_resource_group + node_resource_group=aks.node_resource_group, + pod_identity_profile=create_pod_identity_profile(aks.pod_identity_profile.as_dict()) if aks.pod_identity_profile else None ) +def create_pod_identity_profile(pod_profile): + return dict( + enabled=pod_profile.get('enabled', False), + allow_network_plugin_kubenet=pod_profile.get('allow_network_plugin_kubenet', False), + user_assigned_identities=pod_profile.get('user_assigned_identities') + ) if pod_profile else {} + + def create_network_profiles_dict(network): return dict( network_plugin=network.network_plugin, @@ -715,7 +830,7 @@ api_server_access_profile_spec = dict( ) -class AzureRMManagedCluster(AzureRMModuleBase): +class AzureRMManagedCluster(AzureRMModuleBaseExt): """Configuration class for an Azure RM container service (AKS) resource""" def __init__(self): @@ -777,6 +892,31 @@ class AzureRMManagedCluster(AzureRMModuleBase): ), node_resource_group=dict( type='str' + ), + pod_identity_profile=dict( + type='dict', + options=dict( + enabled=dict(type='bool'), + allow_network_plugin_kubenet=dict(type='bool'), + user_assigned_identities=dict( + type='list', + elements='dict', + options=dict( + name=dict(type='str', required=True), + namespace=dict(type='str', required=True), + binding_selector=dict(type='str'), + identity=dict( + type='dict', + required=True, + options=dict( + resource_id=dict(type='str'), + client_id=dict(type='str'), + object_id=dict(type='str') + ) + ) + ) + ) + ) ) ) @@ -796,6 +936,7 @@ class AzureRMManagedCluster(AzureRMModuleBase): self.api_server_access_profile = None self.addon = None self.node_resource_group = None + self.pod_identity_profile = None required_if = [ ('state', 'present', [ @@ -972,6 +1113,10 @@ class AzureRMManagedCluster(AzureRMModuleBase): if not matched: self.log("Agent Pool not found") to_be_updated = True + if not self.default_compare({}, self.pod_identity_profile, response['pod_identity_profile'], '', dict(compare=[])): + to_be_updated = True + else: + self.pod_identity_profile = response['pod_identity_profile'] if update_agentpool: self.log("Need to update agentpool") @@ -1044,6 +1189,15 @@ class AzureRMManagedCluster(AzureRMModuleBase): else: linux_profile = None + if self.pod_identity_profile: + pod_identity_profile = self.managedcluster_models.ManagedClusterPodIdentityProfile( + enabled=self.pod_identity_profile.get('enabled'), + allow_network_plugin_kubenet=self.pod_identity_profile.get('allow_network_plugin_kubenet'), + user_assigned_identities=self.pod_identity_profile.get('user_assigned_identities') + ) + else: + pod_identity_profile = None + parameters = self.managedcluster_models.ManagedCluster( location=self.location, dns_prefix=self.dns_prefix, @@ -1058,7 +1212,8 @@ class AzureRMManagedCluster(AzureRMModuleBase): aad_profile=self.create_aad_profile_instance(self.aad_profile), api_server_access_profile=self.create_api_server_access_profile_instance(self.api_server_access_profile), addon_profiles=self.create_addon_profile_instance(self.addon), - node_resource_group=self.node_resource_group + node_resource_group=self.node_resource_group, + pod_identity_profile=pod_identity_profile ) # self.log("service_principal_profile : {0}".format(parameters.service_principal_profile)) @@ -1169,10 +1324,12 @@ class AzureRMManagedCluster(AzureRMModuleBase): :return: AKS instance kubeconfig ''' - access_profile = self.managedcluster_client.managed_clusters.get_access_profile(resource_group_name=self.resource_group, - resource_name=self.name, - role_name="clusterUser") - return access_profile.kube_config.decode('utf-8') + try: + access_profile = self.managedcluster_client.managed_clusters.list_cluster_user_credentials(self.resource_group, self.name) + except HttpResponseError as ec: + self.log("Lists the cluster user credentials of a managed cluster Failed, Exception as {0}".format(ec)) + return [] + return [item.value.decode('utf-8') for item in access_profile.kubeconfigs] def create_agent_pool_profile_instance(self, agentpoolprofile): ''' diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aks_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aks_info.py index c97bd893e..2e4fd7a26 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aks_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aks_info.py @@ -37,10 +37,14 @@ options: description: - Show kubeconfig of the AKS cluster. - Note the operation will cost more network overhead, not recommended when listing AKS. + - I(show_kubeconfig=monitoring) to lists the cluster monitoring user credentials of a managed cluster. + - I(show_kubeconfig=admin) to lists the cluster admin credentials of a managed cluster. + - I(show_kubeconfig=user) to lists the cluster user credentials of a managed cluster. type: str choices: - user - admin + - monitoring extends_documentation_fragment: - azure.azcollection.azure @@ -75,6 +79,7 @@ from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common try: from azure.core.exceptions import ResourceNotFoundError + from azure.core.exceptions import HttpResponseError except Exception: # handled in azure_rm_common pass @@ -91,7 +96,7 @@ class AzureRMManagedClusterInfo(AzureRMModuleBase): name=dict(type='str'), resource_group=dict(type='str'), tags=dict(type='list', elements='str'), - show_kubeconfig=dict(type='str', choices=['user', 'admin']), + show_kubeconfig=dict(type='str', choices=['user', 'admin', 'monitoring']), ) self.results = dict( @@ -196,11 +201,29 @@ class AzureRMManagedClusterInfo(AzureRMModuleBase): :return: AKS instance kubeconfig ''' - if not self.show_kubeconfig: - return '' - role_name = 'cluster{0}'.format(str.capitalize(self.show_kubeconfig)) - access_profile = self.managedcluster_client.managed_clusters.get_access_profile(resource_group, name, role_name) - return access_profile.kube_config.decode('utf-8') + if self.show_kubeconfig == 'user': + try: + access_profile = self.managedcluster_client.managed_clusters.list_cluster_user_credentials(self.resource_group, self.name) + except HttpResponseError as ec: + self.log("Lists the cluster user credentials of a managed cluster Failed, Exception as {0}".format(ec)) + return [] + return [item.value.decode('utf-8') for item in access_profile.kubeconfigs] + elif self.show_kubeconfig == 'admin': + try: + access_profile = self.managedcluster_client.managed_clusters.list_cluster_admin_credentials(self.resource_group, self.name) + except HttpResponseError as ec: + self.log("Lists the cluster admin credentials of a managed cluster Failed, Exception as {0}".format(ec)) + return [] + return [item.value.decode('utf-8') for item in access_profile.kubeconfigs] + elif self.show_kubeconfig == 'monitoring': + try: + access_profile = self.managedcluster_client.managed_clusters.list_cluster_monitoring_user_credentials(self.resource_group, self.name) + except HttpResponseError as ec: + self.log("Lists the cluster monitoring credentials of a managed cluster Failed, Exception as {0}".format(ec)) + return [] + return [item.value.decode('utf-8') for item in access_profile.kubeconfigs] + else: + return [] def main(): diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_akscredentials_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_akscredentials_info.py new file mode 100644 index 000000000..95a7fb104 --- /dev/null +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_akscredentials_info.py @@ -0,0 +1,209 @@ +#!/usr/bin/python +# +# Copyright (c) 2024 xuzhang3 (@xuzhang3), Fred-sun (@Fred-sun) +# +# 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 + + +DOCUMENTATION = ''' +--- +module: azure_rm_akscredentials_info + +version_added: "2.3.0" + +short_description: List Azure Kubernetes Service Credentials facts + +description: + - List Azure Kubernetes Service Credentials facts. + +options: + cluster_name: + description: + - Azure Kubernetes Service or all Azure Kubernetes Services. + type: str + required: true + resource_group: + description: + - The resource group to search for the desired Azure Kubernetes Service. + type: str + required: true + show_admin_credentials: + description: + - Whether list Cluster Admin Credentials. + type: bool + default: false + show_user_credentials: + description: + - Whether list Cluster User Credentials. + type: bool + default: false + show_monitor_credentials: + description: + - Whether list Cluster Monitoring User Credentials. + type: bool + default: false + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - xuzhang3 (@xuzhang3) + - Fred Sun (@Fred-sun) +''' + +EXAMPLES = ''' +- name: Get managecluster admin credentials + azure_rm_akscredentials_info: + resource_group: "{{ resource_group }}" + cluster_name: cluter_name + show_admin_credentials: true + +- name: Get managecluster user credentials + azure_rm_akscredentials_info: + resource_group: "{{ resource_group }}" + cluster_name: cluster_name + show_user_credentials: true + +- name: Get managecluster monitor user credentials + azure_rm_akscredentials_info: + resource_group: "{{ resource_group }}" + cluster_name: cluster_name + show_monitor_credentials: true +''' + +RETURN = ''' +cluster_credentials: + description: + - Lists the cluster user, admin or monitoring user credentials of a managed cluster + returned: always + type: complex + contains: + cluster_name: + description: + - Azure Kubernetes Service or all Azure Kubernetes Services. + type: str + returned: always + sample: testcluster01 + resource_group: + description: + - The resource group to search for the desired Azure Kubernetes Service. + type: str + returned: always + sample: + name: + description: + - The name of the credential. + type: str + returned: always + sample: + value: + description: + - Base64-encoded Kubernetes configuration file. + type: str + returned: always + sample: "apiVersion: ************************" +''' + +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + +try: + from azure.core.exceptions import HttpResponseError +except Exception: + # handled in azure_rm_common + pass + +AZURE_OBJECT_CLASS = 'managedClustersCredentials' + + +class AzureRMAksCredentialsInfo(AzureRMModuleBase): + """Utility class to get Azure Kubernetes Service Credentials facts""" + + def __init__(self): + + self.module_args = dict( + cluster_name=dict(type='str', required=True), + resource_group=dict(type='str', required=True), + show_admin_credentials=dict(type='bool', default=False), + show_user_credentials=dict(type='bool', default=False), + show_monitor_credentials=dict(type='bool', default=False), + ) + + self.results = dict( + changed=False, + cluster_credentials=[], + ) + + mutually_exclusive = [('show_admin_credentials', 'show_user_credentials', 'show_monitor_credentials')] + + super(AzureRMAksCredentialsInfo, self).__init__( + derived_arg_spec=self.module_args, + supports_check_mode=True, + supports_tags=False, + mutually_exclusive=mutually_exclusive, + facts_module=True + ) + + def exec_module(self, **kwargs): + + for key in self.module_args: + setattr(self, key, kwargs[key]) + + if self.show_user_credentials: + self.results['cluster_credentials'] = self.get_user_credentials() + elif self.show_admin_credentials: + self.results['cluster_credentials'] = self.get_admin_credentials() + elif self.show_monitor_credentials: + self.results['cluster_credentials'] = self.get_monitor_credentials() + + self.results['resource_group'] = self.resource_group + self.results['cluster_name'] = self.cluster_name + return self.results + + def get_user_credentials(self): + """Get The Azure Kubernetes Service USER Credentials""" + response = None + + try: + response = self.managedcluster_client.managed_clusters.list_cluster_user_credentials(self.resource_group, self.cluster_name) + except HttpResponseError as ec: + self.fail("Lists the cluster user credentials of a managed cluster Failed, Exception as {0}".format(ec)) + return [self.format_response(item) for item in response.kubeconfigs] + + def get_admin_credentials(self): + """Get The Azure Kubernetes Service Admin Credentials""" + response = None + + try: + response = self.managedcluster_client.managed_clusters.list_cluster_admin_credentials(self.resource_group, self.cluster_name) + except HttpResponseError as ec: + self.fail("Lists the cluster admin credentials of a managed cluster Failed, Exception as {0}".format(ec)) + + return [self.format_response(item) for item in response.kubeconfigs] + + def get_monitor_credentials(self): + """Get The Azure Kubernetes Service Monitor Credentials""" + response = None + + try: + response = self.managedcluster_client.managed_clusters.list_cluster_monitoring_user_credentials(self.resource_group, self.cluster_name) + except HttpResponseError as ec: + self.fail("Lists the cluster monitoring credentials of a managed cluster Failed, Exception as {0}".format(ec)) + return [self.format_response(item) for item in response.kubeconfigs] + + def format_response(self, item): + if not item: + return + return dict(name=item.name, value=item.value.decode('utf-8')) + + +def main(): + """Main module execution code path""" + + AzureRMAksCredentialsInfo() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aksversion_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aksversion_info.py index 9e5af42ce..5d4f695a6 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aksversion_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_aksversion_info.py @@ -29,6 +29,13 @@ options: description: - Get the upgrade versions available for a managed Kubernetes cluster version. type: str + allow_preview: + description: + - Whether Kubernetes version is currently in preview. + - If I(allow_preview=True), returns the current preview status of the current Kubernetes version. + - If I(allow_preview=False), returns the Kubernetes version in a non-current preview state. + - If no set C(allow_preview), returns all the avaiable Kubernetes version. + type: bool extends_documentation_fragment: - azure.azcollection.azure @@ -38,6 +45,10 @@ author: ''' EXAMPLES = ''' +- name: Get current preview versions for AKS in location eastus + azure_rm_aksversion_info: + location: eastus + allow_preview: true - name: Get available versions for AKS in location eastus azure_rm_aksversion_info: location: eastus @@ -63,7 +74,8 @@ class AzureRMAKSVersion(AzureRMModuleBase): self.module_args = dict( location=dict(type='str', required=True), - version=dict(type='str') + version=dict(type='str'), + allow_preview=dict(type='bool') ) self.results = dict( @@ -73,6 +85,7 @@ class AzureRMAKSVersion(AzureRMModuleBase): self.location = None self.version = None + self.allow_preview = None super(AzureRMAKSVersion, self).__init__( derived_arg_spec=self.module_args, @@ -104,7 +117,14 @@ class AzureRMAKSVersion(AzureRMModuleBase): response = self.containerservice_client.container_services.list_orchestrators(self.location, resource_type='managedClusters') orchestrators = response.orchestrators for item in orchestrators: - result[item.orchestrator_version] = [x.orchestrator_version for x in item.upgrades] if item.upgrades else [] + if self.allow_preview is None: + result[item.orchestrator_version] = [x.orchestrator_version for x in item.upgrades] if item.upgrades else [] + elif self.allow_preview: + if item.is_preview: + result[item.orchestrator_version] = [x.orchestrator_version for x in item.upgrades] if item.upgrades else [] + else: + if not item.is_preview: + result[item.orchestrator_version] = [x.orchestrator_version for x in item.upgrades] if item.upgrades else [] if version: return result.get(version) or [] else: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagement.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagement.py index 97a7868d1..4e7e359bb 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagement.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagement.py @@ -296,41 +296,30 @@ class AzureApiManagement(AzureRMModuleBaseExt): self.module_arg_spec = dict( resource_group=dict( type='str', - updatable=False, - disposition='resourceGroupName', required=True ), service_name=dict( type='str', - updatable=False, - disposition='serviceName', required=True ), api_id=dict( type='str', - updatable=False, - disposition='apiId', required=True ), description=dict( type='str', - disposition='/properties/description' ), authentication_settings=dict( type='dict', - disposition='/properties/authenticationSettings', options=dict( o_auth2=dict( type='dict', - disposition='oAuth2', options=dict( authorization_server_id=dict( type='str', - disposition='authorizationServerId' ), scope=dict( type='str', - disposition='scope' ) ) ), @@ -339,12 +328,10 @@ class AzureApiManagement(AzureRMModuleBaseExt): options=dict( openid_provider_id=dict( type='str', - disposition='openidProviderId' ), bearer_token_sending_methods=dict( type='list', elements='str', - disposition='bearerTokenSendingMethods', choices=['authorizationHeader', 'query'] ) ) @@ -354,7 +341,6 @@ class AzureApiManagement(AzureRMModuleBaseExt): subscription_key_parameter_names=dict( type='dict', no_log=True, - disposition='/properties/subscriptionKeyParameterNames', options=dict( header=dict( type='str', @@ -368,63 +354,49 @@ class AzureApiManagement(AzureRMModuleBaseExt): ), type=dict( type='str', - disposition='/properties/type', choices=['http', 'soap'] ), api_revision=dict( type='str', - disposition='/properties/apiRevision' ), api_version=dict( type='str', - disposition='/properties/apiVersion' ), is_current=dict( type='bool', - disposition='/properties/isCurrent' ), api_revision_description=dict( type='str', - disposition='/properties/apiRevisionDescription' ), api_version_description=dict( type='str', - disposition='/properties/apiVersionDescription' ), api_version_set_id=dict( type='str', - disposition='/properties/apiVersionSetId', ), subscription_required=dict( type='bool', - disposition='/properties/subscriptionRequired' ), source_api_id=dict( type='str', - disposition='/properties/sourceApiId', ), display_name=dict( type='str', - disposition='/properties/displayName' ), service_url=dict( type='str', - disposition='/properties/serviceUrl' ), path=dict( type='str', - disposition='/properties/*', ), protocols=dict( type='list', elements='str', - disposition='/properties/protocols', choices=['http', 'https'] ), api_version_set=dict( type='dict', - disposition='/properties/apiVersionSet', options=dict( id=dict( type='str' @@ -437,28 +409,23 @@ class AzureApiManagement(AzureRMModuleBaseExt): ), versioning_scheme=dict( type='str', - disposition='versioningScheme', choices=['Segment', 'Query', 'Header'] ), version_query_name=dict( type='str', - disposition='versionQueryName' ), version_header_name=dict( type='str', - disposition='versionHeaderName' ) ) ), value=dict( type='str', - disposition='/properties/*' ), format=dict( type='str', - disposition='/properties/*', choices=['wadl-xml', 'wadl-link-json', 'swagger-json', @@ -471,21 +438,17 @@ class AzureApiManagement(AzureRMModuleBaseExt): ), wsdl_selector=dict( type='dict', - disposition='/properties/wsdlSelector', options=dict( wsdl_service_name=dict( type='str', - disposition='wsdlServiceName' ), wsdl_endpoint_name=dict( type='str', - disposition='wsdlEndpointName' ) ) ), api_type=dict( type='str', - disposition='/properties/apiType', choices=['http', 'soap'] ), state=dict( @@ -507,8 +470,9 @@ class AzureApiManagement(AzureRMModuleBaseExt): self.to_do = Actions.NoAction self.body = {} + self.body['properties'] = {} self.query_parameters = {} - self.query_parameters['api-version'] = '2020-06-01-preview' + self.query_parameters['api-version'] = '2022-08-01' self.header_parameters = {} self.header_parameters['Content-Type'] = 'application/json; charset=utf-8' @@ -527,16 +491,81 @@ class AzureApiManagement(AzureRMModuleBaseExt): if hasattr(self, key): setattr(self, key, kwargs[key]) elif kwargs[key] is not None: - self.body[key] = kwargs[key] + if key == 'description': + self.body['properties']['description'] = kwargs[key] + elif key == 'authentication_settings': + self.body['properties']['authenticationSettings'] = {} + if kwargs[key].get('o_auth2') is not None: + self.body['properties']['authenticationSettings']['oAuth2'] = {} + for item in kwargs[key]['o_auth2'].keys(): + if item == 'authorization_server_id': + authorization_id = kwargs[key]['o_auth2']['authorization_server_id'] + self.body['properties']['authenticationSettings']['oAuth2']['authorizationServerId'] = authorization_id + elif item == 'scope': + self.body['properties']['authenticationSettings']['oAuth2']['scope'] = kwargs[key]['o_auth2']['scope'] + elif kwargs[key].get('openid') is not None: + self.body['properties']['authenticationSettings']['openid'] = {} + for item in kwargs[key]['openid'].keys(): + if item == 'openid_provider_id' and kwargs[key]['openid'].get('openid_provider_id') is not None: + openid_pro = kwargs[key]['openid'].get('openid_provider_id') + self.body['properties']['authenticationSettings']['openid']['openidProviderId'] = openid_pro + elif item == 'bearer_token_sending_methods' and kwargs[key]['openid'].get('bearer_token_sending_methods') is not None: + bearer_token = kwargs[key]['openid']['bearer_token_sending_methods'] + self.body['properties']['authenticationSettings']['openid']['bearerTokenSendingMethods'] = bearer_token + elif key == 'subscription_key_parameter_names': + self.body['properties']['subscriptionKeyParameterNames'] = kwargs[key] + elif key == 'type': + self.body['properties']['type'] = kwargs[key] + elif key == 'api_revision': + self.body['properties']['apiRevision'] = kwargs[key] + elif key == 'api_version': + self.body['properties']['apiVersion'] = kwargs[key] + elif key == 'is_current': + self.body['properties']['isCurrent'] = kwargs[key] + elif key == 'api_revision_description': + self.body['properties']['apiRevisionDescription'] = kwargs[key] + elif key == 'api_version_description': + self.body['properties']['apiVersionDescription'] = kwargs[key] + elif key == 'api_version_set_id': + self.body['properties']['apiVersionSetId'] = kwargs[key] + elif key == 'subscription_required': + self.body['properties']['subscriptionRequired'] = kwargs[key] + elif key == 'source_api_id': + self.body['properties']['sourceApiId'] = kwargs[key] + elif key == 'display_name': + self.body['properties']['displayName'] = kwargs[key] + elif key == 'service_url': + self.body['properties']['serviceUrl'] = kwargs[key] + elif key == 'protocols': + self.body['properties']['protocols'] = kwargs[key] + elif key == 'api_version_set': + self.body['properties']['apiVersionSet'] = {} + for item in kwargs[key].keys(): + if item == 'versioning_scheme': + self.body['properties']['apiVersionSet'] = kwargs[key].get('versioning_scheme') + elif item == 'version_query_name': + self.body['properties']['versionQueryName'] = kwargs[key].get('version_query_name') + elif item == 'version_header_name': + self.body['properties']['versionHeaderName'] = kwargs[key].get('version_header_name') + else: + self.body['properties'][item] = kwargs[key].get(item) + elif key == 'wsdl_selector': + self.body['properties']['wsdlSelector'] = {} + for item in kwargs[key].keys(): + if item == 'wsdl_service_name': + self.body['properties']['wsdlSelector']['wsdlServiceName'] = kwargs[key].get(item) + if item == 'wsdl_endpoint_name': + self.body['properties']['wsdlSelector']['wsdlEndpointName'] = kwargs[key].get(item) + elif key == 'api_type': + self.body['properties']['apiType'] = kwargs[key] + else: + self.body['properties'][key] = kwargs[key] - # https://docs.microsoft.com/en-us/azure/templates/microsoft.apimanagement/service/apis - self.inflate_parameters(self.module_arg_spec, self.body, 0) self.url = self.get_url() old_response = None response = None self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) old_response = self.get_resource() @@ -552,11 +581,75 @@ class AzureApiManagement(AzureRMModuleBaseExt): if self.state == 'absent': self.to_do = Actions.Delete else: - modifiers = {} - self.create_compare_modifiers(self.module_arg_spec, '', modifiers) - self.results['modifiers'] = modifiers - self.results['compare'] = [] - if not self.default_compare(modifiers, self.body, old_response, '', self.results): + if self.body['properties'].get('description') is not None and \ + self.body['properties']['description'] != old_response['properties']['description']: + self.to_do = Actions.Update + elif self.body['properties'].get('authenticationSettings') is not None: + if old_response['properties'].get('authenticationSettings') is None: + self.to_do = Actions.Update + elif (self.body['properties']['authenticationSettings'].get('oAuth2') is not None and + self.body['properties']['authenticationSettings']['oAuth2'] != old_response['properties']['authenticationSettings'].get('oAuth2')): + self.to_do = Actions.Update + elif (self.body['properties']['authenticationSettings'].get('openid') is not None and + self.body['properties']['authenticationSettings']['openid'] != old_response['properties']['authenticationSettings'].get('openid')): + self.to_do = Actions.Update + elif self.body['properties'].get('subscriptionKeyParameterNames') is not None: + tt = old_response['properties'] + if old_response['properties'].get('subscriptionKeyParameterNames') is None: + self.to_do = Actions.Update + elif (not all(self.body['properties']['subscriptionKeyParameterNames'].get(item) == tt['subscriptionKeyParameterNames'].get(item) + for item in self.body['properties']['subscriptionKeyParameterNames'].keys())): + self.to_do = Actions.Update + elif (self.body['properties'].get('apiRevision') is not None and + self.body['properties']['apiRevision'] != old_response['properties'].get('apiRevision')): + self.to_do = Actions.Update + elif (self.body['properties'].get('apiVersion') is not None and + self.body['properties']['apiVersion'] != old_response['properties'].get('apiVersion')): + self.to_do == Actions.Update + elif (self.body['properties'].get('isCurrent') is not None and + self.body['properties']['isCurrent'] != old_response['properties'].get('isCurrent')): + self.to_do = Actions.Update + elif (self.body['properties'].get('apiRevisionDescription') is not None and self.body['properties']['apiRevisionDescription'] != + old_response['properties'].get('apiRevisionDescription')): + self.to_do = Actions.Update + elif (self.body['properties'].get('apiVersionDescription') is not None and + self.body['properties']['apiVersionDescription'] != old_response['properties'].get('apiVersionDescription')): + self.to_do = Actions.Update + elif (self.body['properties'].get('apiVersionSetId') is not None and + self.body['properties']['apiVersionSetId'] != old_response['properties'].get('apiVersionSetId')): + self.to_do = Actions.Update + elif (self.body['properties'].get('subscriptionRequired') is not None and + self.body['properties']['subscriptionRequired'] != old_response['properties'].get('subscriptionRequired')): + self.to_do = Actions.Update + elif (self.body['properties'].get('sourceApiId') is not None and + self.body['properties']['sourceApiId'] != old_response['properties'].get('sourceApiId')): + self.to_do = Actions.Update + elif (self.body['properties'].get('displayName') is not None and + self.body['properties']['displayName'] != old_response['properties'].get('displayName')): + self.to_do = Actions.Update + elif (self.body['properties'].get('serviceUrl') is not None and + self.body['properties']['serviceUrl'] != old_response['properties'].get('serviceUrl')): + self.to_do = Actions.Update + elif self.body['properties'].get('path') is not None and self.body['properties']['path'] != old_response['properties'].get('path'): + self.to_do = Actions.Update + elif (self.body['properties'].get('protocols') is not None and + self.body['properties']['protocols'] != old_response['properties'].get('protocols')): + self.to_do = Actions.Update + elif self.body['properties'].get('type') is not None and self.body['properties']['type'] != old_response['properties'].get('type'): + self.to_do = Actions.Update + elif self.body['properties'].get('apiType') is not None and self.body['properties']['apiType'] != old_response['properties'].get('apiType'): + self.to_do = Actions.Update + elif self.body['properties'].get('value') is not None and self.body['properties']['value'] != old_response['properties'].get('value'): + self.to_do = Actions.Update + elif self.body['properties'].get('format') is not None and self.body['properties']['format'] != old_response['properties'].get('format'): + self.to_do = Actions.Update + elif (self.body['properties'].get('wsdlSelector') is not None and + not all(self.body['properties']['wsdlSelector'][item] == old_response['properties']['wsdlSelector'].get(item) + for item in self.body['properties']['wsdlSelector'].keys())): + self.to_do = Actions.Update + elif (self.body['properties'].get('apiVersionSet') is not None and + not all(self.body['properties']['apiVersionSet'][item] == old_response['properties']['apiVersionSet'].get(item) + for item in self.body['properties']['apiVersionSet'].keys())): self.to_do = Actions.Update if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): @@ -605,10 +698,12 @@ class AzureApiManagement(AzureRMModuleBaseExt): except Exception as exc: self.log('Error while creating/updating the Api instance.') self.fail('Error creating the Api instance: {0}'.format(str(exc))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagement_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagement_info.py index 30120b5e5..51e99b733 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagement_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagement_info.py @@ -139,7 +139,7 @@ class AzureApiManagementInfo(AzureRMModuleBaseExt): self.status_code = [200] self.query_parameters = {} - self.query_parameters['api-version'] = '2020-06-01-preview' + self.query_parameters['api-version'] = '2022-08-01' self.header_parameters = {} self.header_parameters['Content-Type'] = 'application/json; charset=utf-8' @@ -155,7 +155,6 @@ class AzureApiManagementInfo(AzureRMModuleBaseExt): self.body[key] = kwargs[key] self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if (self.resource_group is not None and diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagementservice.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagementservice.py index 8c694bec4..6d8a0aaf9 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagementservice.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagementservice.py @@ -113,32 +113,23 @@ class AzureRMApiManagementService(AzureRMModuleBaseExt): self.module_arg_spec = dict( resource_group=dict( type='str', - updatable=False, - disposition='resourceGroupName', required=True ), name=dict( type='str', - updatable=False, - disposition='serviceName', required=True ), location=dict( type='str', - updatable=False, - disposition='location' ), publisher_name=dict( type='str', - disposition='/properties/publisherName' ), publisher_email=dict( type='str', - disposition='/properties/publisherEmail' ), sku_name=dict( type='str', - disposition='/sku/name', choices=['Developer', 'Standard', 'Premium', @@ -147,7 +138,6 @@ class AzureRMApiManagementService(AzureRMModuleBaseExt): ), sku_capacity=dict( type='int', - disposition='/sku/capacity' ), state=dict( type='str', @@ -168,8 +158,10 @@ class AzureRMApiManagementService(AzureRMModuleBaseExt): self.to_do = Actions.NoAction self.body = {} + self.body['properties'] = {} + self.body['sku'] = {} self.query_parameters = {} - self.query_parameters['api-version'] = '2020-06-01-preview' + self.query_parameters['api-version'] = '2022-08-01' self.header_parameters = {} self.header_parameters['Content-Type'] = 'application/json; charset=utf-8' @@ -182,15 +174,21 @@ class AzureRMApiManagementService(AzureRMModuleBaseExt): if hasattr(self, key): setattr(self, key, kwargs[key]) elif kwargs[key] is not None: - self.body[key] = kwargs[key] - - self.inflate_parameters(self.module_arg_spec, self.body, 0) + if key == 'publisher_name': + self.body['properties']['publisherName'] = kwargs[key] + elif key == 'publisher_email': + self.body['properties']['publisherEmail'] = kwargs[key] + elif key == 'sku_name': + self.body['sku']['name'] = kwargs[key] + elif key == 'sku_capacity': + self.body['sku']['capacity'] = kwargs[key] + else: + self.body[key] = kwargs[key] old_response = None response = None self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) resource_group = self.get_resource_group(self.resource_group) @@ -225,12 +223,15 @@ class AzureRMApiManagementService(AzureRMModuleBaseExt): if self.state == 'absent': self.to_do = Actions.Delete else: - modifiers = {} - self.create_compare_modifiers(self.module_arg_spec, '', modifiers) - self.results['modifiers'] = modifiers - self.results['compare'] = [] - if not self.default_compare(modifiers, self.body, old_response, '', self.results): - self.to_do = Actions.Update + for key in self.body.keys(): + if key == 'properties': + for key in self.body['properties'].keys(): + if self.body['properties'][key] != old_response['properties'].get(key): + self.to_do = Actions.Update + elif key == 'sku': + for key in self.body['sku'].keys(): + if self.body['sku'][key] != old_response['sku'].get(key): + self.to_do = Actions.Update self.body['location'] = self.location if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): @@ -272,23 +273,34 @@ class AzureRMApiManagementService(AzureRMModuleBaseExt): def create_update_resource(self): # Creating / Updating the ApiManagementService instance. try: - response = self.mgmt_client.query(self.url, - 'PUT', - self.query_parameters, - self.header_parameters, - self.body, - self.status_code, - 600, - 30) + if self.to_do == Actions.Create: + response = self.mgmt_client.query(self.url, + 'PUT', + self.query_parameters, + self.header_parameters, + self.body, + self.status_code, + 600, + 30) + else: + response = self.mgmt_client.query(self.url, + 'PATCH', + self.query_parameters, + self.header_parameters, + self.body, + self.status_code, + 600, + 30) except Exception as exc: self.log('Error attempting to create the ApiManagementService instance.') self.fail('Error creating the ApiManagementService instance: {0}'.format(str(exc))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} - pass + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagementservice_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagementservice_info.py index 663e87e78..5d686c98d 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagementservice_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_apimanagementservice_info.py @@ -143,7 +143,7 @@ class AzureRMApiManagementServiceInfo(AzureRMModuleBaseExt): self.status_code = [200] self.query_parameters = {} - self.query_parameters['api-version'] = '2020-06-01-preview' + self.query_parameters['api-version'] = '2022-08-01' self.header_parameters = {} self.header_parameters['Content-Type'] = 'application/json; charset=utf-8' @@ -156,7 +156,6 @@ class AzureRMApiManagementServiceInfo(AzureRMModuleBaseExt): setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if (self.resource_group is not None and self.name is not None): diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_appgateway.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_appgateway.py index f95766fa9..8d70dda89 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_appgateway.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_appgateway.py @@ -519,6 +519,24 @@ options: - Whether host header should be picked from the host name of the backend HTTP settings. Default value is false. type: bool default: False + port: + description: + - Custom port which will be used for probing the backend servers. + - The valid value ranges from 1 to 65535. + - In case not set, port from http settings will be used. + - This property is valid for C(Standard_v2) and C(WAF_v2) only. + type: int + match: + description: + - Criterion for classifying a healthy probe response. + type: dict + suboptions: + status_codes: + description: + - Allowed ranges of healthy status codes. + - Default range of healthy status codes is 200-399. + type: list + elements: str backend_http_settings_collection: description: - Backend http settings of the application gateway resource. @@ -1419,6 +1437,54 @@ EXAMPLES = ''' max_request_body_size_in_kb: 128 file_upload_limit_in_mb: 100 +- name: Create application gateway with multi parameters + azure_rm_appgateway: + resource_group: myResourceGroup + name: myappgateway + sku: + name: standard_v2 + tier: standard_v2 + capacity: 2 + gateway_ip_configurations: + - subnet: + id: "{{ subnet_id }}" + name: app_gateway_ip_config + frontend_ip_configurations: + - name: sample_gateway_frontend_ip_config + public_ip_address: "pip{{ rpfx }}" + frontend_ports: + - port: 80 + name: http_frontend_port + backend_address_pools: + - name: test_backend_address_pool # empty pool which will receive attachment to NIC. + backend_http_settings_collection: + - port: 80 + protocol: http + cookie_based_affinity: enabled + name: sample_appgateway_http_settings + http_listeners: + - frontend_ip_configuration: sample_gateway_frontend_ip_config + frontend_port: http_frontend_port + protocol: http + name: http_listener + probes: + - name: testprobes01 + protocol: http + path: '/' + timeout: 30 + host: testazure + interval: 90 + port: 80 + match: + status_codes: + - 200 + request_routing_rules: + - rule_type: basic + backend_address_pool: test_backend_address_pool + backend_http_settings: sample_appgateway_http_settings + http_listener: http_listener + name: rule1 + - name: Stop an Application Gateway instance azure_rm_appgateway: resource_group: myResourceGroup @@ -1517,6 +1583,11 @@ ssl_policy_spec = dict( ) +match_spec = dict( + status_codes=dict(type='list', elements='str') +) + + probe_spec = dict( host=dict(type='str'), interval=dict(type='int'), @@ -1525,7 +1596,9 @@ probe_spec = dict( protocol=dict(type='str', choices=['http', 'https']), timeout=dict(type='int'), unhealthy_threshold=dict(type='int'), - pick_host_name_from_backend_http_settings=dict(type='bool', default=False) + pick_host_name_from_backend_http_settings=dict(type='bool', default=False), + port=dict(type='int'), + match=dict(type='dict', options=match_spec) ) @@ -2239,6 +2312,8 @@ class AzureRMApplicationGateways(AzureRMModuleBase): self.parameters["web_application_firewall_configuration"] = kwargs[key] elif key == "enable_http2": self.parameters["enable_http2"] = kwargs[key] + elif key == "tags": + self.parameters["tags"] = kwargs[key] response = None @@ -2272,7 +2347,7 @@ class AzureRMApplicationGateways(AzureRMModuleBase): (old_response['operational_state'] == 'Running' and self.gateway_state == 'started')): self.to_do = Actions.NoAction elif (self.parameters['location'] != old_response['location'] or - self.parameters['enable_http2'] != old_response['enable_http2'] or + self.parameters['enable_http2'] != old_response.get('enable_http2', False) or self.parameters['sku']['name'] != old_response['sku']['name'] or self.parameters['sku']['tier'] != old_response['sku']['tier'] or self.parameters['sku'].get('capacity', None) != old_response['sku'].get('capacity', None) or @@ -2296,6 +2371,11 @@ class AzureRMApplicationGateways(AzureRMModuleBase): else: self.to_do = Actions.NoAction + update_tags, new_tags = self.update_tags(old_response.get('tags')) + if update_tags: + self.to_do = Actions.Update + self.parameters["tags"] = new_tags + if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): self.log("Need to Create / Update the Application Gateway instance") diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_azurefirewall.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_azurefirewall.py index f960f024d..692628317 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_azurefirewall.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_azurefirewall.py @@ -354,40 +354,30 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): self.module_arg_spec = dict( resource_group=dict( type='str', - disposition='resource_group_name', required=True ), name=dict( type='str', - disposition='azure_firewall_name', required=True ), location=dict( type='str', - updatable=False, - disposition='/', - comparison='location' ), application_rule_collections=dict( type='list', elements='dict', - disposition='/properties/applicationRuleCollections', options=dict( priority=dict( type='int', - disposition='properties/*' ), action=dict( type='str', choices=['allow', 'deny'], - disposition='properties/action/type', - pattern='camelize' ), rules=dict( type='list', elements='raw', - disposition='properties/*', options=dict( name=dict( type='str' @@ -398,7 +388,6 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): source_addresses=dict( type='list', elements='str', - disposition='sourceAddresses' ), protocols=dict( type='list', @@ -406,7 +395,6 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): options=dict( type=dict( type='str', - disposition='protocolType' ), port=dict( type='str' @@ -416,12 +404,10 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): target_fqdns=dict( type='list', elements='raw', - disposition='targetFqdns' ), fqdn_tags=dict( type='list', elements='raw', - disposition='fqdnTags' ) ) ), @@ -433,23 +419,18 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): nat_rule_collections=dict( type='list', elements='dict', - disposition='/properties/natRuleCollections', options=dict( priority=dict( type='int', - disposition='properties/*' ), action=dict( type='str', - disposition='properties/action/type', choices=['snat', 'dnat'], - pattern='camelize' ), rules=dict( type='list', elements='dict', - disposition='properties/*', options=dict( name=dict( type='str' @@ -460,17 +441,14 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): source_addresses=dict( type='list', elements='str', - disposition='sourceAddresses' ), destination_addresses=dict( type='list', elements='str', - disposition='destinationAddresses' ), destination_ports=dict( type='list', elements='str', - disposition='destinationPorts' ), protocols=dict( type='list', @@ -478,11 +456,9 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): ), translated_address=dict( type='str', - disposition='translatedAddress' ), translated_port=dict( type='str', - disposition='translatedPort' ) ) ), @@ -494,23 +470,18 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): network_rule_collections=dict( type='list', elements='dict', - disposition='/properties/networkRuleCollections', options=dict( priority=dict( type='int', - disposition='properties/*' ), action=dict( type='str', choices=['allow', 'deny'], - disposition='properties/action/type', - pattern='camelize' ), rules=dict( type='list', elements='dict', - disposition='properties/*', options=dict( name=dict( type='str' @@ -525,17 +496,14 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): source_addresses=dict( type='list', elements='str', - disposition='sourceAddresses' ), destination_addresses=dict( type='list', elements='str', - disposition='destinationAddresses' ), destination_ports=dict( type='list', elements='str', - disposition='destinationPorts' ) ) ), @@ -547,22 +515,12 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): ip_configurations=dict( type='list', elements='dict', - disposition='/properties/ipConfigurations', options=dict( subnet=dict( type='raw', - disposition='properties/subnet/id', - pattern=('/subscriptions/{subscription_id}/resourceGroups' - '/{resource_group}/providers/Microsoft.Network' - '/virtualNetworks/{virtual_network_name}/subnets' - '/{name}') ), public_ip_address=dict( type='raw', - disposition='properties/publicIPAddress/id', - pattern=('/subscriptions/{subscription_id}/resourceGroups' - '/{resource_group}/providers/Microsoft.Network' - '/publicIPAddresses/{name}') ), name=dict( type='str' @@ -579,6 +537,7 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): self.resource_group = None self.name = None self.body = {} + self.body['properties'] = {} self.results = dict(changed=False) self.mgmt_client = None @@ -587,7 +546,6 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): self.status_code = [200, 201, 202] self.to_do = Actions.NoAction - self.body = {} self.query_parameters = {} self.query_parameters['api-version'] = '2018-11-01' self.header_parameters = {} @@ -602,15 +560,171 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): if hasattr(self, key): setattr(self, key, kwargs[key]) elif kwargs[key] is not None: - self.body[key] = kwargs[key] - - self.inflate_parameters(self.module_arg_spec, self.body, 0) + if key == 'application_rule_collections': + self.body['properties']['applicationRuleCollections'] = [] + for item in kwargs[key]: + app_rule = dict(properties={}) + if item.get('priority') is not None: + app_rule['properties']['priority'] = item['priority'] + if item.get('action') is not None: + app_rule['properties']['action'] = dict(type=item['action']) + if item.get('name') is not None: + app_rule['name'] = item['name'] + if item.get('rules') is not None: + app_rule['properties']['rules'] = [] + for value in item['rules']: + rule_value = {} + if value.get('name') is not None: + rule_value['name'] = value['name'] + if value.get('description') is not None: + rule_value['description'] = value['description'] + if value.get('source_addresses') is not None: + rule_value['sourceAddresses'] = value.get('source_addresses') + if value.get('target_fqdns') is not None: + rule_value['targetFqdns'] = value.get('target_fqdns') + if value.get('fqdn_tags') is not None: + rule_value['fqdnTags'] = value.get('fqdn_tags') + if value.get('protocols') is not None: + rule_value['protocols'] = [] + for pp in value['protocols']: + pro = {} + if pp.get('type') is not None: + pro['protocolType'] = pp.get('type') + if pp.get('port') is not None: + pro['port'] = pp.get('port') + rule_value['protocols'].append(pro) + app_rule['properties']['rules'].append(rule_value) + self.body['properties']['applicationRuleCollections'].append(app_rule) + elif key == 'nat_rule_collections': + self.body['properties']['natRuleCollections'] = [] + for item in kwargs[key]: + nat_rule = dict(properties={}) + if item.get('priority') is not None: + nat_rule['properties']['priority'] = item['priority'] + if item.get('action') is not None: + nat_rule['properties']['action'] = dict(type=item['action']) + if item.get('name') is not None: + nat_rule['name'] = item['name'] + if item.get('rules') is not None: + nat_rule['properties']['rules'] = [] + for value in item['rules']: + nat_value = {} + if value.get('name') is not None: + nat_value['name'] = value.get('name') + if value.get('description') is not None: + nat_value['description'] = value.get('description') + if value.get('source_addresses') is not None: + nat_value['sourceAddresses'] = value.get('source_addresses') + if value.get('destination_addresses') is not None: + nat_value['destinationAddresses'] = value.get('destination_addresses') + if value.get('destination_ports') is not None: + nat_value['destinationPorts'] = value.get('destination_ports') + if value.get('protocols') is not None: + nat_value['protocols'] = value.get('protocols') + if value.get('translated_address') is not None: + nat_value['translatedAddress'] = value.get('translated_address') + if value.get('translated_port') is not None: + nat_value['translatedPort'] = value.get('translated_port') + nat_rule['properties']['rules'].append(nat_value) + self.body['properties']['natRuleCollections'].append(nat_rule) + elif key == 'network_rule_collections': + self.body['properties']['networkRuleCollections'] = [] + for item in kwargs[key]: + network_rule = dict(properties={}) + if item.get('priority') is not None: + network_rule['properties']['priority'] = item['priority'] + if item.get('action') is not None: + network_rule['properties']['action'] = dict(type=item['action']) + if item.get('name') is not None: + network_rule['name'] = item['name'] + if item.get('rules') is not None: + network_rule['properties']['rules'] = [] + for value in item['rules']: + net_value = {} + if value.get('name') is not None: + net_value['name'] = value.get('name') + if value.get('description') is not None: + net_value['description'] = value.get('description') + if value.get('source_addresses') is not None: + net_value['sourceAddresses'] = value.get('source_addresses') + if value.get('destination_addresses') is not None: + net_value['destinationAddresses'] = value.get('destination_addresses') + if value.get('destination_ports') is not None: + net_value['destinationPorts'] = value.get('destination_ports') + if value.get('protocols') is not None: + net_value['protocols'] = value.get('protocols') + network_rule['properties']['rules'].append(net_value) + self.body['properties']['networkRuleCollections'].append(network_rule) + elif key == 'ip_configurations': + self.body['properties']['ipConfigurations'] = [] + for item in kwargs[key]: + ipconfig = dict(properties={}) + if item.get('subnet') is not None: + ipconfig['properties']['subnet'] = {} + if isinstance(item['subnet'], str): + ipconfig['properties']['subnet']['id'] = item['subnet'] + elif isinstance(item['subnet'], dict): + if item['subnet'].get('id') is not None: + ipconfig['properties']['subnet']['id'] = item['subnet'].get('id') + elif (item['subnet'].get('resource_group') is not None and item['subnet'].get('name') is not None and + item['subnet'].get('virtual_network_name') is not None): + ipconfig['properties']['subnet']['id'] = ('/subscriptions/' + + self.subscription_id + + '/resourceGroups/' + + item['subnet'].get('resource_group') + + '/providers/Microsoft.Network/virtualNetworks/' + + item['subnet'].get('virtual_network_name') + + '/subnets/' + + item['subnet'].get('name')) + elif item['subnet'].get('name') is not None and item['subnet'].get('virtual_network_name') is not None: + ipconfig['properties']['subnet']['id'] = ('/subscriptions/' + + self.subscription_id + + '/resourceGroups/' + + self.resource_group + + '/providers/Microsoft.Network/virtualNetworks/' + + item['subnet'].get('virtual_network_name') + + '/subnets/' + + item['subnet'].get('name')) + else: + self.fail("The ip_configuration's subnet config error") + else: + self.fail("The ip_configuration's subnet config error") + if item.get('public_ip_address') is not None: + ipconfig['properties']['publicIPAddress'] = {} + if isinstance(item.get('public_ip_address'), str): + ipconfig['properties']['publicIPAddress']['id'] = item.get('public_ip_address') + elif isinstance(item.get('public_ip_address'), dict): + if item['public_ip_address'].get('id') is not None: + ipconfig['properties']['publicIPAddress']['id'] = item['public_ip_address'].get('id') + elif item['public_ip_address'].get('resource_group') is not None and item['public_ip_address'].get('name') is not None: + ipconfig['properties']['publicIPAddress']['id'] = ('/subscriptions/' + + self.subscription_id + + '/resourceGroups/' + + item['public_ip_address'].get('resource_group') + + '/providers/Microsoft.Network/publicIPAddresses/' + + item['public_ip_address'].get('name')) + elif item['public_ip_address'].get('name') is not None: + ipconfig['properties']['publicIPAddress']['id'] = ('/subscriptions/' + + self.subscription_id + + '/resourceGroups/' + + self.resource_group + + '/providers/Microsoft.Network/publicIPAddresses/' + + item['public_ip_address'].get('name')) + else: + self.fail("The ip_configuration's public ip address config error") + else: + self.fail("The ip_configuration's public ip address config error") + + if item.get('name') is not None: + ipconfig['name'] = item['name'] + self.body['properties']['ipConfigurations'].append(ipconfig) + else: + self.body[key] = kwargs[key] old_response = None response = None self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) resource_group = self.get_resource_group(self.resource_group) @@ -642,11 +756,12 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): if self.state == 'absent': self.to_do = Actions.Delete else: - modifiers = {} - self.create_compare_modifiers(self.module_arg_spec, '', modifiers) - self.results['modifiers'] = modifiers - self.results['compare'] = [] - if not self.default_compare(modifiers, self.body, old_response, '', self.results): + update_tags, new_tags = self.update_tags(old_response.get('tags')) + if update_tags: + self.to_do = Actions.Update + self.body['tags'] = new_tags + + if not self.default_compare({}, self.body, old_response, '', dict(compare=[])): self.to_do = Actions.Update if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): @@ -705,10 +820,12 @@ class AzureRMAzureFirewalls(AzureRMModuleBaseExt): self.log('Error attempting to create the AzureFirewall instance.') self.fail('Error creating the AzureFirewall instance: {0}'.format(str(exc))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_azurefirewall_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_azurefirewall_info.py index d86932693..a565ef886 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_azurefirewall_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_azurefirewall_info.py @@ -140,7 +140,6 @@ class AzureRMAzureFirewallsInfo(AzureRMModuleBase): setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if (self.resource_group is not None and self.name is not None): diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_backupazurevm.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_backupazurevm.py index 77d6d92ed..863839329 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_backupazurevm.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_backupazurevm.py @@ -255,7 +255,6 @@ class BackupAzureVM(AzureRMModuleBaseExt): response = None self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) changed = False @@ -296,10 +295,12 @@ class BackupAzureVM(AzureRMModuleBaseExt): self.fail( 'Error in creating/updating protection for Azure VM {0}'.format(str(e))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response @@ -322,10 +323,12 @@ class BackupAzureVM(AzureRMModuleBaseExt): self.log('Error attempting to stop protection.') self.fail('Error in disabling the protection: {0}'.format(str(e))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response @@ -348,10 +351,12 @@ class BackupAzureVM(AzureRMModuleBaseExt): self.log('Error attempting to delete backup.') self.fail('Error deleting the azure backup: {0}'.format(str(e))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response @@ -375,10 +380,12 @@ class BackupAzureVM(AzureRMModuleBaseExt): self.fail( 'Error while taking on-demand backup: {0}'.format(str(e))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_backupazurevm_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_backupazurevm_info.py index b4ba22fc1..90ece65cb 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_backupazurevm_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_backupazurevm_info.py @@ -130,7 +130,6 @@ class BackupAzureVMInfo(AzureRMModuleBaseExt): response = None self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) response = self.get_recovery_point_info() @@ -156,10 +155,12 @@ class BackupAzureVMInfo(AzureRMModuleBaseExt): self.log('Error in fetching recovery point.') self.fail('Error in fetching recovery point {0}'.format(str(e))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_batchaccount.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_batchaccount.py index ac237294c..b5ff4d923 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_batchaccount.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_batchaccount.py @@ -140,25 +140,19 @@ class AzureRMBatchAccount(AzureRMModuleBaseExt): type='str' ), location=dict( - type='str', - updatable=False, - disposition='/' + type='str' ), auto_storage_account=dict( type='raw' ), key_vault=dict( type='raw', - no_log=True, - updatable=False, - disposition='/' + no_log=True ), pool_allocation_mode=dict( default='batch_service', type='str', - choices=['batch_service', 'user_subscription'], - updatable=False, - disposition='/' + choices=['batch_service', 'user_subscription'] ), state=dict( type='str', @@ -212,8 +206,7 @@ class AzureRMBatchAccount(AzureRMModuleBaseExt): response = None self.mgmt_client = self.get_mgmt_svc_client(BatchManagementClient, - base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True) + base_url=self._cloud_environment.endpoints.resource_manager) old_response = self.get_batchaccount() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_batchaccount_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_batchaccount_info.py index fb61248e4..80930d6db 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_batchaccount_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_batchaccount_info.py @@ -112,9 +112,7 @@ class AzureRMBatchAccountInfo(AzureRMModuleBaseExt): response = [] self.mgmt_client = self.get_mgmt_svc_client(BatchManagementClient, - base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True) - + base_url=self._cloud_environment.endpoints.resource_manager) if self.resource_group is not None and self.name is not None: response = [self.get_batchaccount()] elif self.resource_group is not None: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnendpoint.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnendpoint.py index 2f3a3d76f..ad4cb1303 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnendpoint.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnendpoint.py @@ -659,7 +659,6 @@ class AzureRMCdnendpoint(AzureRMModuleBase): if not self.cdn_client: self.cdn_client = self.get_mgmt_svc_client(CdnManagementClient, base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True, api_version='2017-04-02') return self.cdn_client diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnendpoint_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnendpoint_info.py index 9f89408d0..46a759847 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnendpoint_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnendpoint_info.py @@ -232,7 +232,6 @@ class AzureRMCdnEndpointInfo(AzureRMModuleBase): self.cdn_client = self.get_mgmt_svc_client(CdnManagementClient, base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True, api_version='2017-04-02') if self.name: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnprofile.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnprofile.py index 96761228f..ebac6f22f 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnprofile.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnprofile.py @@ -290,7 +290,6 @@ class AzureRMCdnprofile(AzureRMModuleBase): if not self.cdn_client: self.cdn_client = self.get_mgmt_svc_client(CdnManagementClient, base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True, api_version='2017-04-02') return self.cdn_client diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnprofile_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnprofile_info.py index 92b9b7957..f4c599854 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnprofile_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cdnprofile_info.py @@ -253,7 +253,6 @@ class AzureRMCdnprofileInfo(AzureRMModuleBase): if not self.cdn_client: self.cdn_client = self.get_mgmt_svc_client(CdnManagementClient, base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True, api_version='2017-04-02') return self.cdn_client diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cosmosdbaccount.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cosmosdbaccount.py index d1f3dd987..98d2c7149 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cosmosdbaccount.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cosmosdbaccount.py @@ -426,7 +426,6 @@ class AzureRMCosmosDBAccount(AzureRMModuleBase): response = None self.mgmt_client = self.get_mgmt_svc_client(CosmosDBManagementClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) resource_group = self.get_resource_group(self.resource_group) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cosmosdbaccount_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cosmosdbaccount_info.py index 61414272a..f80b2835e 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cosmosdbaccount_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_cosmosdbaccount_info.py @@ -431,7 +431,6 @@ class AzureRMCosmosDBAccountInfo(AzureRMModuleBase): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(CosmosDBManagementClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if self.name is not None: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_datalakestore.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_datalakestore.py index eaecb9df5..b46907339 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_datalakestore.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_datalakestore.py @@ -504,6 +504,7 @@ class AzureRMDatalakeStore(AzureRMModuleBase): supports_tags=True) def exec_module(self, **kwargs): + self.module.deprecate("The azure_rm_datalakestore.py will deprecated. Azure Data Lake Storage Gen1 retired on February 29,2024", version=(2.3, )) for key in list(self.module_arg_spec.keys()) + ['tags']: setattr(self, key, kwargs[key]) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_datalakestore_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_datalakestore_info.py index 2417ff74a..8444a4c1c 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_datalakestore_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_datalakestore_info.py @@ -312,6 +312,7 @@ class AzureRMDatalakeStoreInfo(AzureRMModuleBase): supports_tags=False) def exec_module(self, **kwargs): + self.module.deprecate("The azure_rm_datalakestore_info.py will deprecated. Azure Data Lake Storage Gen1 retired on February 29,2024", version=(2.3, )) for key in self.module_arg_spec: setattr(self, key, kwargs[key]) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_deployment.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_deployment.py index 07cf93d8e..ef32b19dc 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_deployment.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_deployment.py @@ -84,7 +84,7 @@ options: description: - If I(state=present), template will be created. - If I(state=present) and deployment exists, it will be updated. - - If I(state=absent), the resource group will be removed. + - If I(state=absent), the deployment resource will be deleted. default: present type: str choices: @@ -498,7 +498,7 @@ class AzureRMDeploymentManager(AzureRMModuleBase): else: try: if self.get_resource_group(self.resource_group): - self.destroy_resource_group() + self.destroy_deployment_resource() self.results['changed'] = True self.results['msg'] = "deployment deleted" except Exception: @@ -572,12 +572,12 @@ class AzureRMDeploymentManager(AzureRMModuleBase): return deployment_result - def destroy_resource_group(self): + def destroy_deployment_resource(self): """ Destroy the targeted resource group """ try: - result = self.rm_client.resource_groups.begin_delete(self.resource_group) + result = self.rm_client.deployments.begin_delete(self.resource_group, self.name) result.wait() # Blocking wait till the delete is finished except Exception as e: if e.status_code == 404 or e.status_code == 204: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlab.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlab.py index 1424be8c9..4d0ce4a36 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlab.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlab.py @@ -157,7 +157,6 @@ class AzureRMDevTestLab(AzureRMModuleBase): self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True, api_version='2018-10-15') resource_group = self.get_resource_group(self.resource_group) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlab_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlab_info.py index 6add55e59..91dcf737a 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlab_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlab_info.py @@ -184,7 +184,6 @@ class AzureRMDevTestLabInfo(AzureRMModuleBase): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if self.resource_group is not None: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabarmtemplate_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabarmtemplate_info.py index 783a272dd..cf03aac99 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabarmtemplate_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabarmtemplate_info.py @@ -162,7 +162,6 @@ class AzureRMDtlArmTemplateInfo(AzureRMModuleBase): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if self.name: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabartifact_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabartifact_info.py index 9ec729257..35a084e5d 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabartifact_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabartifact_info.py @@ -175,7 +175,6 @@ class AzureRMArtifactInfo(AzureRMModuleBase): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if self.name: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabartifactsource.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabartifactsource.py index 87f19bbd9..7ae9290ea 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabartifactsource.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabartifactsource.py @@ -220,7 +220,6 @@ class AzureRMDevTestLabArtifactsSource(AzureRMModuleBase): self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True, api_version='2018-10-15') old_response = self.get_devtestlabartifactssource() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabartifactsource_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabartifactsource_info.py index 036a3da45..0b3dbeaba 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabartifactsource_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabartifactsource_info.py @@ -185,7 +185,6 @@ class AzureRMDtlArtifactSourceInfo(AzureRMModuleBase): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if self.name: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabcustomimage.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabcustomimage.py index f24c6f7e5..6017ca2ed 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabcustomimage.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabcustomimage.py @@ -205,7 +205,6 @@ class AzureRMDtlCustomImage(AzureRMModuleBase): response = None self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) old_response = self.get_customimage() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabcustomimage_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabcustomimage_info.py index 2c6d559aa..e5c434f4a 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabcustomimage_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabcustomimage_info.py @@ -162,7 +162,6 @@ class AzureRMDtlCustomImageInfo(AzureRMModuleBase): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if self.name: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabenvironment.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabenvironment.py index 214ddbbe1..7fd3737fd 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabenvironment.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabenvironment.py @@ -190,7 +190,6 @@ class AzureRMDtlEnvironment(AzureRMModuleBase): response = None self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) resource_group = self.get_resource_group(self.resource_group) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabenvironment_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabenvironment_info.py index 4648ed1b7..31e2d1b42 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabenvironment_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabenvironment_info.py @@ -175,7 +175,6 @@ class AzureRMDtlEnvironmentInfo(AzureRMModuleBase): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if self.name: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabpolicy.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabpolicy.py index 2a3f2f953..7d19dd813 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabpolicy.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabpolicy.py @@ -200,7 +200,6 @@ class AzureRMDtlPolicy(AzureRMModuleBase): response = None self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) resource_group = self.get_resource_group(self.resource_group) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabpolicy_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabpolicy_info.py index a8f3b104a..8801d09ad 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabpolicy_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabpolicy_info.py @@ -173,7 +173,6 @@ class AzureRMDtlPolicyInfo(AzureRMModuleBase): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if self.name: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabschedule.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabschedule.py index 060b3baa0..1907794c8 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabschedule.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabschedule.py @@ -172,7 +172,6 @@ class AzureRMSchedule(AzureRMModuleBase): response = None self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) resource_group = self.get_resource_group(self.resource_group) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabschedule_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabschedule_info.py index e366e0f99..c5ded5ffd 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabschedule_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabschedule_info.py @@ -157,7 +157,6 @@ class AzureRMDtlScheduleInfo(AzureRMModuleBase): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if self.name: self.results['schedules'] = self.get() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualmachine.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualmachine.py index 62e7db77a..9a21fc532 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualmachine.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualmachine.py @@ -393,7 +393,6 @@ class AzureRMVirtualMachine(AzureRMModuleBase): response = None self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) old_response = self.get_virtualmachine() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualmachine_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualmachine_info.py index 24398136e..8a3692e2a 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualmachine_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualmachine_info.py @@ -253,7 +253,6 @@ class AzureRMDtlVirtualMachineInfo(AzureRMModuleBase): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if self.name: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualnetwork.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualnetwork.py index a47a8c8f9..15e3b67f6 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualnetwork.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualnetwork.py @@ -157,7 +157,6 @@ class AzureRMDevTestLabVirtualNetwork(AzureRMModuleBase): self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True, api_version='2018-10-15') resource_group = self.get_resource_group(self.resource_group) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualnetwork_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualnetwork_info.py index 23392468b..0f6d59e02 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualnetwork_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_devtestlabvirtualnetwork_info.py @@ -152,7 +152,6 @@ class AzureRMDevTestLabVirtualNetworkInfo(AzureRMModuleBase): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if self.name: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_dnsrecordset.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_dnsrecordset.py index 32448964a..1ac9983b4 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_dnsrecordset.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_dnsrecordset.py @@ -360,8 +360,10 @@ class AzureRMRecordSet(AzureRMModuleBase): resource_group=dict(type='str', required=True), relative_name=dict(type='str', required=True), zone_name=dict(type='str', required=True), - record_type=dict(choices=RECORD_ARGSPECS.keys(), required=True, type='str'), - record_mode=dict(choices=['append', 'purge'], default='purge'), + record_type=dict(choices=['A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SRV', 'TXT', 'SOA', 'CAA'], + required=True, + type='str'), + record_mode=dict(type='str', choices=['append', 'purge'], default='purge'), state=dict(choices=['present', 'absent'], default='present', type='str'), time_to_live=dict(type='int', default=3600), records=dict(type='list', elements='dict'), diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_gallery.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_gallery.py index cb902ace7..9eb8ce397 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_gallery.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_gallery.py @@ -85,24 +85,17 @@ class AzureRMGalleries(AzureRMModuleBaseExt): self.module_arg_spec = dict( resource_group=dict( type='str', - updatable=False, - disposition='resourceGroupName', required=True ), name=dict( type='str', - updatable=False, - disposition='galleryName', required=True ), location=dict( - type='str', - updatable=False, - disposition='/' + type='str' ), description=dict( type='str', - disposition='/properties/*' ), state=dict( type='str', @@ -123,6 +116,7 @@ class AzureRMGalleries(AzureRMModuleBaseExt): self.to_do = Actions.NoAction self.body = {} + self.body['properties'] = {} self.query_parameters = {} self.query_parameters['api-version'] = '2019-07-01' self.header_parameters = {} @@ -133,19 +127,19 @@ class AzureRMGalleries(AzureRMModuleBaseExt): supports_tags=True) def exec_module(self, **kwargs): - for key in list(self.module_arg_spec.keys()): + for key in list(self.module_arg_spec.keys()) + ['tags']: if hasattr(self, key): setattr(self, key, kwargs[key]) elif kwargs[key] is not None: - self.body[key] = kwargs[key] - - self.inflate_parameters(self.module_arg_spec, self.body, 0) + if key == 'description': + self.body['properties']['description'] = kwargs[key] + else: + self.body[key] = kwargs[key] old_response = None response = None self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) resource_group = self.get_resource_group(self.resource_group) @@ -180,28 +174,36 @@ class AzureRMGalleries(AzureRMModuleBaseExt): if self.state == 'absent': self.to_do = Actions.Delete else: - modifiers = {} - self.create_compare_modifiers(self.module_arg_spec, '', modifiers) - self.results['modifiers'] = modifiers - self.results['compare'] = [] - if not self.default_compare(modifiers, self.body, old_response, '', self.results): + if self.body.get('properties') is not None and self.body['properties']['description'] != old_response['properties']['description']: self.to_do = Actions.Update - self.body['properties'].pop('identifier', None) + else: + self.body['properties']['description'] = old_response['properties']['description'] - if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): - self.log('Need to Create / Update the Gallery instance') + update_tags, new_tags = self.update_tags(old_response.get('tags')) + if update_tags: + self.to_do = Actions.Update + self.body['tags'] = new_tags + if self.to_do == Actions.Create: + self.log('Need to Create the Gallery instance') if self.check_mode: self.results['changed'] = True return self.results - - response = self.create_update_resource() - + response = self.create_resource() # if not old_response: self.results['changed'] = True # else: # self.results['changed'] = old_response.__ne__(response) - self.log('Creation / Update done') + self.log('Creation done') + elif self.to_do == Actions.Update: + self.log('Need to Update the Gallery instance') + if self.check_mode: + self.results['changed'] = True + return self.results + response = self.update_resource() + # if not old_response: + self.results['changed'] = True + self.log('Update done') elif self.to_do == Actions.Delete: self.log('Gallery instance deleted') self.results['changed'] = True @@ -225,8 +227,33 @@ class AzureRMGalleries(AzureRMModuleBaseExt): return self.results - def create_update_resource(self): - # self.log('Creating / Updating the Gallery instance {0}'.format(self.)) + def update_resource(self): + # self.log('Updating the Gallery instance {0}'.format(self.)) + + try: + response = self.mgmt_client.query(self.url, + 'PATCH', + self.query_parameters, + self.header_parameters, + self.body, + self.status_code, + 600, + 30) + except Exception as exc: + self.log('Error attempting to update the Gallery instance.') + self.fail('Error updating the Gallery instance: {0}'.format(str(exc))) + + if hasattr(response, 'body'): + response = json.loads(response.body()) + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Updating fail, no match message return, return info as {0}".format(response)) + + return response + + def create_resource(self): + # self.log('Creating the Gallery instance {0}'.format(self.)) try: response = self.mgmt_client.query(self.url, @@ -241,10 +268,12 @@ class AzureRMGalleries(AzureRMModuleBaseExt): self.log('Error attempting to create the Gallery instance.') self.fail('Error creating the Gallery instance: {0}'.format(str(exc))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create fail, no match message return, return info as {0}".format(response)) return response diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_gallery_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_gallery_info.py index cd02bec28..d144c46fb 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_gallery_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_gallery_info.py @@ -128,7 +128,6 @@ class AzureRMGalleriesInfo(AzureRMModuleBase): setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if (self.resource_group is not None and self.name is not None): @@ -232,6 +231,8 @@ class AzureRMGalleriesInfo(AzureRMModuleBase): return [self.format_item(x) for x in results['value']] if results['value'] else [] def format_item(self, item): + if not item: + return d = { 'id': item['id'], 'name': item['name'], diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimage.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimage.py index c14aedd4a..9a4542458 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimage.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimage.py @@ -76,6 +76,14 @@ options: - V1 - V2 type: str + architecture: + description: + - This property allows you to specify the hardware architecture of the Virtual Machines. + - Arm64 is only supported with Hyper V Version 2. + choices: + - Arm64 + - x64 + type: str end_of_life_date: description: - The end of life date of the gallery Image Definition. @@ -162,6 +170,22 @@ options: description: - The product ID. type: str + features: + description: + - A list of gallery image features. + type: list + elements: dict + suboptions: + name: + description: + - The name of the gallery image feature. + type: str + required: True + value: + description: + - The value of the gallery image feature. + type: str + required: True state: description: - Assert the state of the GalleryImage. @@ -219,73 +243,60 @@ class AzureRMGalleryImages(AzureRMModuleBaseExt): self.module_arg_spec = dict( resource_group=dict( type='str', - updatable=False, - disposition='resourceGroupName', required=True ), gallery_name=dict( type='str', - updatable=False, - disposition='galleryName', required=True ), name=dict( type='str', - updatable=False, - disposition='galleryImageName', required=True ), location=dict( type='str', - updatable=False, - disposition='/' ), description=dict( type='str', - disposition='/properties/*' ), eula=dict( type='str', - disposition='/properties/*' ), privacy_statement_uri=dict( type='str', - disposition='/properties/privacyStatementUri' ), release_note_uri=dict( type='str', - disposition='/properties/releaseNoteUri' ), os_type=dict( type='str', - disposition='/properties/osType', choices=['windows', 'linux'] ), os_state=dict( type='str', - disposition='/properties/osState', choices=['generalized', 'specialized'] ), hypervgeneration=dict( type='str', - disposition='/properties/hyperVGeneration', choices=['V1', 'V2'] ), + architecture=dict( + type='str', + choices=['Arm64', + 'x64'] + ), end_of_life_date=dict( type='str', - disposition='/properties/endOfLifeDate' ), identifier=dict( type='dict', - disposition='/properties/*', options=dict( publisher=dict( type='str', required=True, - updatable=False ), offer=dict( type='str', @@ -299,11 +310,9 @@ class AzureRMGalleryImages(AzureRMModuleBaseExt): ), recommended=dict( type='dict', - disposition='/properties/*', options=dict( v_cpus=dict( type='dict', - disposition='vCPUs', options=dict( min=dict( type='int' @@ -328,18 +337,15 @@ class AzureRMGalleryImages(AzureRMModuleBaseExt): ), disallowed=dict( type='dict', - disposition='/properties/*', options=dict( disk_types=dict( type='list', elements='str', - disposition='diskTypes' ) ) ), purchase_plan=dict( type='dict', - disposition='/properties/purchasePlan', options=dict( name=dict( type='str' @@ -352,6 +358,20 @@ class AzureRMGalleryImages(AzureRMModuleBaseExt): ) ) ), + features=dict( + type='list', + elements='dict', + options=dict( + name=dict( + type='str', + required=True + ), + value=dict( + type='str', + required=True + ) + ) + ), state=dict( type='str', default='present', @@ -372,8 +392,9 @@ class AzureRMGalleryImages(AzureRMModuleBaseExt): self.to_do = Actions.NoAction self.body = {} + self.body['properties'] = {} self.query_parameters = {} - self.query_parameters['api-version'] = '2019-07-01' + self.query_parameters['api-version'] = '2022-03-03' self.header_parameters = {} self.header_parameters['Content-Type'] = 'application/json; charset=utf-8' @@ -382,19 +403,51 @@ class AzureRMGalleryImages(AzureRMModuleBaseExt): supports_tags=True) def exec_module(self, **kwargs): - for key in list(self.module_arg_spec.keys()): + for key in list(self.module_arg_spec.keys()) + ['tags']: if hasattr(self, key): setattr(self, key, kwargs[key]) elif kwargs[key] is not None: - self.body[key] = kwargs[key] - - self.inflate_parameters(self.module_arg_spec, self.body, 0) + if key == 'description': + self.body['properties']['description'] = kwargs[key] + elif key == 'eula': + self.body['properties']['eula'] = kwargs[key] + elif key == 'privacy_statement_uri': + self.body['properties']['privacyStatementUri'] = kwargs[key] + elif key == 'release_note_uri': + self.body['properties']['releaseNoteUri'] = kwargs[key] + elif key == 'os_type': + self.body['properties']['osType'] = kwargs[key] + elif key == 'os_state': + self.body['properties']['osState'] = kwargs[key] + elif key == 'hypervgeneration': + self.body['properties']['hyperVGeneration'] = kwargs[key] + elif key == 'architecture': + self.body['properties']['architecture'] = kwargs[key] + elif key == 'end_of_life_date': + self.body['properties']['endOfLifeDate'] = kwargs[key] + elif key == 'identifier': + self.body['properties']['identifier'] = kwargs[key] + elif key == 'recommended': + self.body['properties']['recommended'] = {} + for item in kwargs[key].keys(): + if item == 'v_cpus': + self.body['properties']['recommended']['vCPUs'] = kwargs[key].get('v_cpus') + elif item == 'memory': + self.body['properties']['recommended']['memory'] = kwargs[key].get('memory') + elif key == 'disallowed': + self.body['properties']['disallowed'] = {} + self.body['properties']['disallowed']['diskTypes'] = kwargs[key].get('disk_types') + elif key == 'purchase_plan': + self.body['properties']['purchasePlan'] = kwargs[key] + elif key == 'features': + self.body['properties']['features'] = kwargs[key] + else: + self.body[key] = kwargs[key] old_response = None response = None self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) resource_group = self.get_resource_group(self.resource_group) @@ -432,11 +485,60 @@ class AzureRMGalleryImages(AzureRMModuleBaseExt): if self.state == 'absent': self.to_do = Actions.Delete else: - modifiers = {} - self.create_compare_modifiers(self.module_arg_spec, '', modifiers) - self.results['modifiers'] = modifiers - self.results['compare'] = [] - if not self.default_compare(modifiers, self.body, old_response, '', self.results): + if self.body['properties'].get('description') is not None and \ + self.body['properties']['description'] != old_response['properties'].get('description'): + self.to_do = Actions.Update + elif self.body['properties'].get('eula') is not None and self.body['properties']['eula'] != old_response['properties'].get('eula'): + self.to_do = Actions.Update + elif (self.body['properties'].get('privacyStatementUri') is not None and + self.body['properties']['privacyStatementUri'] != old_response['properties'].get('privacyStatementUri')): + self.to_do = Actions.Update + elif (self.body['properties'].get('releaseNoteUri') is not None and + self.body['properties']['releaseNoteUri'] != old_response['properties'].get('releaseNoteUri')): + self.to_do = Actions.Update + elif (self.body['properties'].get('osType') is not None and + self.body['properties']['osType'].lower() != old_response['properties'].get('osType', '').lower()): + self.to_do = Actions.Update + elif (self.body['properties'].get('osState') is not None and + self.body['properties']['osState'].lower() != old_response['properties'].get('osState', '').lower()): + self.to_do = Actions.Update + elif (self.body['properties'].get('hyperVGeneration') is not None and + self.body['properties']['hyperVGeneration'] != old_response['properties'].get('hyperVGeneration')): + self.to_do = Actions.Update + elif (self.body['properties'].get('architecture') is not None and + self.body['properties']['architecture'] != old_response['properties'].get('architecture')): + self.to_do = Actions.Update + elif (self.body['properties'].get('endOfLifeDate') is not None and + self.body['properties']['endOfLifeDate'] != old_response['properties'].get('endOfLifeDate')): + self.to_do = Actions.Update + elif (self.body['properties'].get('identifier') is not None and + self.body['properties']['identifier'].get('offer') != old_response['properties']['identifier'].get('offer') or + self.body['properties']['identifier'].get('sku') != old_response['properties']['identifier'].get('sku')): + self.to_do = Actions.Update + elif self.body['properties'].get('recommended') is not None: + if self.body['properties']['recommended'].get('vCPUS') is not None: + for item in self.body['properties']['recommended']['vCPUS'].keys(): + if self.body['properties']['recommended']['vCPUS'].get(item) != old_response['properties']['recommended']['vCPUS'].get(item): + self.to_do = Actions.Update + elif (self.body['properties']['recommended'].get('memory') is not None and + not all(self.body['properties']['recommended']['memory'].get(item) == old_response['properties']['recommended']['memory'].get(item) + for item in self.body['properties']['recommended']['memory'].keys())): + self.to_do = Actions.Update + elif (self.body['properties'].get('disallowed') is not None and + self.body['properties']['disallowed'].get('diskTypes') != old_response['properties']['disallowed'].get('diskTypes')): + self.to_do = Actions.Update + elif self.body['properties'].get('purchasePlan') is not None: + for item in self.body['properties']['purchasePlan'].keys(): + if self.body['properties']['purchasePlan'][item] != old_response['properties']['purchasePlan'].get(item): + self.to_do = Actions.Update + elif self.body['properties'].get('features') is not None: + if old_response['properties'].get('features') is None: + self.to_do = Actions.Update + else: + if not all(item in old_response['properties']['features'] for item in self.body['properties']['features']): + self.to_do = Actions.Update + update_tags, self.body['tags'] = self.update_tags(old_response.get('tags')) + if update_tags: self.to_do = Actions.Update if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): @@ -492,10 +594,12 @@ class AzureRMGalleryImages(AzureRMModuleBaseExt): self.log('Error attempting to create the GalleryImage instance.') self.fail('Error creating the GalleryImage instance: {0}'.format(str(exc))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimage_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimage_info.py index 0f1c5020f..1b48f4f22 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimage_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimage_info.py @@ -159,7 +159,6 @@ class AzureRMGalleryImagesInfo(AzureRMModuleBase): setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if (self.resource_group is not None and diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimageversion.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimageversion.py index 9d61bf874..66a4dea6b 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimageversion.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimageversion.py @@ -125,6 +125,62 @@ options: description: - Storage account type. type: str + encryption: + description: + - Allows users to provide customer managed keys for encrypting the OS and data disks in the gallery artifact. + type: dict + suboptions: + data_disk_images: + description: + - A list of encryption specifications for data disk images. + type: list + elements: dict + suboptions: + disk_encryption_set_id: + description: + - A relative URI containing the resource ID of the disk encryption set. + type: str + lun: + description: + - This property specifies the logical unit number of the data disk. + - This value is used to identify data disks within the Virtual Machine and + therefore must be unique for each data disk attached to the Virtual Machine. + type: int + os_disk_image: + description: + - Contains encryption settings for an OS disk image. + type: dict + suboptions: + disk_encryption_set_id: + description: + - A relative URI containing the resource ID of the disk encryption set. + type: str + security_profile: + description: + - This property specifies the security profile of an OS disk image. + type: dict + suboptions: + confidential_vm_encryption_type: + description: + - Confidential VM encryption types. + type: dict + suboptions: + encrypted_vm_guest_state_only_with_pmk: + description: + - VM Guest State Only with PMK. + type: str + encrypted_with_cmk: + description: + - Encrypted with CMK. + type: str + encrypted_with_pmk: + description: + - Encrypted with PMK. + type: str + secure_vm_disk_encryption_set_id: + description: + - Secure VM disk encryption set id. + type: str managed_image: description: - Managed image reference, could be resource ID, or dictionary containing I(resource_group) and I(name) @@ -274,74 +330,40 @@ class AzureRMGalleryImageVersions(AzureRMModuleBaseExt): self.module_arg_spec = dict( resource_group=dict( type='str', - updatable=False, - disposition='resourceGroupName', required=True ), gallery_name=dict( type='str', - updatable=False, - disposition='galleryName', required=True ), gallery_image_name=dict( type='str', - updatable=False, - disposition='galleryImageName', required=True ), name=dict( type='str', - updatable=False, - disposition='galleryImageVersionName', required=True ), tags=dict( type='dict', - updatable=False, - disposition='tags', - comparison='tags' ), location=dict( type='str', - updatable=False, - disposition='/', - comparison='location' ), storage_profile=dict( type='dict', - updatable=False, - disposition='/properties/storageProfile', - comparison='ignore', options=dict( source_image=dict( type='raw', - disposition='source/id', - purgeIfNone=True, - pattern=[('/subscriptions/{subscription_id}/resourceGroups' - '/{resource_group}/providers/Microsoft.Compute' - '/images/{name}'), - ('/subscriptions/{subscription_id}/resourceGroups' - '/{resource_group}/providers/Microsoft.Compute' - '/galleries/{gallery_name}/images/{gallery_image_name}' - '/versions/{version}')] ), os_disk=dict( type='dict', - disposition='osDiskImage', - purgeIfNone=True, - comparison='ignore', options=dict( source=dict( type='raw', - disposition='source/id', - pattern=('/subscriptions/{subscription_id}/resourceGroups' - '/{resource_group}/providers/Microsoft.Compute' - '/snapshots/{name}') ), host_caching=dict( type='str', - disposition='hostCaching', default="None", choices=["ReadOnly", "ReadWrite", "None"] ) @@ -350,22 +372,15 @@ class AzureRMGalleryImageVersions(AzureRMModuleBaseExt): data_disks=dict( type='list', elements='raw', - disposition='dataDiskImages', - purgeIfNone=True, options=dict( lun=dict( type='int' ), source=dict( type='raw', - disposition="source/id", - pattern=('/subscriptions/{subscription_id}/resourceGroups' - '/{resource_group}/providers/Microsoft.Compute' - '/snapshots/{name}') ), host_caching=dict( type='str', - disposition='hostCaching', default="None", choices=["ReadOnly", "ReadWrite", "None"] ) @@ -375,57 +390,87 @@ class AzureRMGalleryImageVersions(AzureRMModuleBaseExt): ), publishing_profile=dict( type='dict', - disposition='/properties/publishingProfile', options=dict( target_regions=dict( type='list', elements='raw', - disposition='targetRegions', options=dict( name=dict( type='str', required=True, - comparison='location' ), regional_replica_count=dict( type='int', - disposition='regionalReplicaCount' ), storage_account_type=dict( type='str', - disposition='storageAccountType' + ), + encryption=dict( + type='dict', + options=dict( + data_disk_images=dict( + type='list', + elements='dict', + options=dict( + disk_encryption_set_id=dict( + type='str', + ), + lun=dict( + type='int' + ) + ) + ), + os_disk_image=dict( + type='dict', + options=dict( + disk_encryption_set_id=dict( + type='str', + ), + security_profile=dict( + type='dict', + options=dict( + confidential_vm_encryption_type=dict( + type='dict', + options=dict( + encrypted_vm_guest_state_only_with_pmk=dict( + type='str', + ), + encrypted_with_cmk=dict( + type='str', + ), + encrypted_with_pmk=dict( + type='str', + ) + ) + ), + secure_vm_disk_encryption_set_id=dict( + type='str', + ) + ) + ) + ) + ) + ) ) ) ), managed_image=dict( type='raw', - pattern=('/subscriptions/{subscription_id}/resourceGroups' - '/{resource_group}/providers/Microsoft.Compute' - '/images/{name}'), - comparison='ignore' ), snapshot=dict( type='raw', - pattern=('/subscriptions/{subscription_id}/resourceGroups' - '/{resource_group}/providers/Microsoft.Compute' - '/snapshots/{name}'), - comparison='ignore' ), replica_count=dict( type='int', - disposition='replicaCount' ), exclude_from_latest=dict( type='bool', - disposition='excludeFromLatest' ), end_of_life_date=dict( type='str', - disposition='endOfLifeDate' ), storage_account_type=dict( type='str', - disposition='storageAccountType', choices=['Standard_LRS', 'Standard_ZRS'] ) @@ -453,8 +498,9 @@ class AzureRMGalleryImageVersions(AzureRMModuleBaseExt): self.to_do = Actions.NoAction self.body = {} + self.body['properties'] = {} self.query_parameters = {} - self.query_parameters['api-version'] = '2019-07-01' + self.query_parameters['api-version'] = '2022-03-03' self.header_parameters = {} self.header_parameters['Content-Type'] = 'application/json; charset=utf-8' @@ -469,9 +515,182 @@ class AzureRMGalleryImageVersions(AzureRMModuleBaseExt): if key == 'tags': self.body[key] = kwargs[key] elif kwargs[key] is not None: - self.body[key] = kwargs[key] - - self.inflate_parameters(self.module_arg_spec, self.body, 0) + if key == 'location': + self.body['location'] = kwargs[key] + elif key == 'storage_profile': + self.body['properties']['storageProfile'] = {} + if kwargs[key].get('source_image') is not None: + self.body['properties']['storageProfile']['source'] = {} + if isinstance(kwargs[key].get('source_image'), str): + self.body['properties']['storageProfile']['source']['id'] = kwargs[key].get('source_image') + elif isinstance(kwargs[key].get('source_image'), dict): + if kwargs[key]['source_image'].get('id') is not None: + self.body['properties']['storageProfile']['source']['id'] = kwargs[key]['source_image'].get('id') + if kwargs[key]['source_image'].get('resource_group') is not None and kwargs[key]['source_image'].get('name') is not None: + self.body['properties']['storageProfile']['source']['id'] = ('/subscriptions/' + + self.subscription_id + + '/resourceGroups/' + + kwargs[key]['source_image'].get('resource_group') + + '/providers/Microsoft.Compute/images/' + + kwargs[key]['source_image'].get('name')) + elif (kwargs[key]['source_image'].get('resource_group') is not None and + kwargs[key]['source_image'].get('gallery_name') is not None and + kwargs[key]['source_image'].get('gallery_image_name') is not None and kwargs[key]['source_image'].get('version') is not None): + self.body['properties']['storageProfile']['source']['id'] = ('/subscriptions/' + + self.subscription_id + + '/resourceGroups/' + + kwargs[key]['source_image'].get('resource_group') + + '/providers/Microsoft.Compute/galleries/' + + kwargs[key]['source_image'].get('gallery_name') + + '/images/' + + kwargs[key]['source_image'].get('gallery_image_name') + + '/versions/' + + kwargs[key]['source_image'].get('version')) + else: + self.fail("The source_image parameters config errors") + else: + self.fail("The source_image parameters config errors") + if kwargs[key].get('os_disk') is not None: + self.body['properties']['storageProfile']['osDiskImage'] = {} + if kwargs[key]['os_disk'].get('host_caching') is not None: + self.body['properties']['storageProfile']['osDiskImage']['hostCaching'] = kwargs[key]['os_disk'].get('host_caching') + if kwargs[key]['os_disk'].get('source') is not None: + self.body['properties']['storageProfile']['osDiskImage']['source'] = {} + if isinstance(kwargs[key]['os_disk']['source'], str): + self.body['properties']['storageProfile']['osDiskImage']['source']['id'] = kwargs[key]['os_disk']['source'] + elif isinstance(kwargs[key]['os_disk']['source'], dict): + if kwargs[key]['os_disk']['source'].get('id') is not None: + self.body['properties']['storageProfile']['osDiskImage']['source']['id'] = kwargs[key]['os_disk']['source'].get('id') + if kwargs[key]['os_disk']['source'].get('resource_group') is not None and \ + kwargs[key]['os_disk']['source'].get('name') is not None: + resource_group = kwargs[key]['os_disk']['source'].get('resource_group') + self.body['properties']['storageProfile']['osDiskImage']['source']['id'] = ('/subscriptions/' + + self.subscription_id + + '/resourceGroups/' + + resource_group + + '/providers/Microsoft.Compute/snapshots/' + + kwargs[key]['os_disk']['source'].get('name')) + else: + self.fail("The os_disk.source parameters config errors") + + else: + self.fail("The os_disk.source parameters config errors") + + if kwargs[key].get('data_disks') is not None: + self.body['properties']['storageProfile']['dataDiskImages'] = [] + data_disk = {} + for item in kwargs[key].get('data_disks'): + if item.get('lun') is not None: + data_disk['lun'] = item['lun'] + if item.get('source') is not None: + data_disk['source'] = {} + if isinstance(item.get('source'), str): + data_disk['source']['id'] = item.get('source') + elif isinstance(item.get('source'), dict): + if item['source'].get('id') is not None: + data_disk['source']['id'] = item['source'].get('id') + elif item['source'].get('resource_group') is not None and item['source'].get('name') is not None: + data_disk['source']['id'] = ('/subscriptions/' + + self.subscription_id + + '/resourceGroups/' + + item['source'].get('resource_group') + + '/providers/Microsoft.Compute/snapshots/' + + item['source'].get('name')) + else: + self.fail("The data_disk.source parameters config errors") + else: + self.fail("The data_disk.source parameters config errors") + if item.get('host_caching') is not None: + data_disk['hostCaching'] = item['host_caching'] + elif key == 'publishing_profile': + self.body['properties']['publishingProfile'] = {} + if kwargs['publishing_profile'].get('target_regions') is not None: + self.body['properties']['publishingProfile']['targetRegions'] = [] + for item in kwargs['publishing_profile']['target_regions']: + target_regions = {} + for value in item.keys(): + if value == 'name': + target_regions[value] = item[value] + elif value == 'regional_replica_count': + target_regions['regionalReplicaCount'] = item[value] + elif value == 'storage_account_type': + target_regions['storageAccountType'] = item[value] + elif value == 'encryption': + target_regions['encryption'] = {} + if item[value].get('data_disk_images') is not None: + target_regions['encryption']['dataDiskImages'] = [] + for tt in item[value]['data_disk_images']: + disk_image = {} + if tt.get('lun') is not None: + disk_image['lun'] = tt['lun'] + if tt.get('disk_encryption_set_id') is not None: + disk_image['diskEncryptionSetId'] = tt['disk_encryption_set_id'] + target_regions['encryption']['dataDiskImages'].append(disk_image) + + if item['encryption'].get('os_disk_image') is not None: + target_regions['encryption']['osDiskImage'] = {} + if item['encryption']['os_disk_image'].get('disk_encryption_set_id') is not None: + disk_encryption_set_id = item['encryption']['os_disk_image']['disk_encryption_set_id'] + target_regions['encryption']['osDiskImage']['diskEncryptionSetId'] = disk_encryption_set_id + if item['encryption']['os_disk_image'].get('security_profile') is not None: + target_regions['encryption']['osDiskImage']['securityProfile'] = {} + if item['encryption']['os_disk_image']['security_profile'].get('secure_vm_disk_encryption_set_id') is not None: + secure_id = item['encryption']['os_disk_image']['security_profile']['secure_vm_disk_encryption_set_id'] + target_regions['encryption']['osDiskImage']['securityProfile']['secureVMDiskEncryptionSetId'] = secure_id + + if item['encryption']['os_disk_image']['security_profile'].get('confidential_vm_encryption_type') is not None: + target_regions['encryption']['osDiskImage']['securityProfile']['confidentialVMEncryptionType'] = {} + security = item['encryption']['os_disk_image']['security_profile']['confidential_vm_encryption_type'] + tt = target_regions['encryption']['osDiskImage']['securityProfile']['confidentialVMEncryptionType'] + if security.get('encrypted_vm_guest_state_only_with_pmk') is not None: + tt['EncryptedVMGuestStateOnlyWithPmk'] = security.get('encrypted_vm_guest_state_only_with_pmk') + if security.get('encrypted_with_cmk') is not None: + tt['EncryptedWithCmk'] = security.get('encrypted_with_cmk') + if security.get('encrypted_with_pmk') is not None: + tt['EncryptedWithPmk'] = security.get('encrypted_with_pmk') + self.body['properties']['publishingProfile']['targetRegions'].append(target_regions) + if kwargs[key].get('managed_image') is not None: + if isinstance(kwargs[key]['managed_image'], str): + self.body['properties']['publishingProfile']['managed_image'] = kwargs[key]['managed_image'] + elif isinstance(kwargs[key]['managed_image'], dict): + if kwargs[key]['managed_image'].get('id') is not None: + self.body['properties']['publishingProfile']['managed_image'] = kwargs[key]['managed_image']['id'] + elif kwargs[key]['managed_image'].get('resource_group') is not None and kwargs[key]['managed_image'].get('name') is not None: + self.body['properties']['publishingProfile']['managed_image'] = ('/subscriptions/' + + self.subscription_id + + '/resourceGroups/' + + kwargs[key]['managed_image'].get('resource_group') + + '/providers/Microsoft.Compute/images/' + + kwargs[key]['managed_image'].get('name')) + else: + self.fail("The managed_image parameters config errors") + else: + self.fail("The managed_image parameters config errors") + if kwargs[key].get('snapshot') is not None: + if isinstance(kwargs[key].get('snapshot'), str): + self.body['properties']['publishingProfile']['snapshot'] = kwargs[key].get('snapshot') + elif isinstance(kwargs[key].get('snapshot'), dict): + if kwargs[key]['snapshot'].get('id') is not None: + self.body['properties']['publishingProfile']['snapshot'] = kwargs[key]['snapshot'].get('id') + elif kwargs[key]['snapshot'].get('resource_group') is not None and kwargs[key]['snapshot'].get('name') is not None: + self.body['properties']['publishingProfile']['snapshot'] = ('/subscriptions/' + + self.subscription_id + + '/resourceGroups/' + + kwargs[key]['snapshot'].get('resource_group') + + '/providers/Microsoft.Compute/snapshots/' + + kwargs[key]['snapshot'].get('name')) + else: + self.fail("The managed_image parameters config errors") + else: + self.fail("The managed_image parameters config errors") + if kwargs[key].get('replica_count') is not None: + self.body['properties']['publishingProfile']['replicaCount'] = kwargs[key].get('replica_count') + if kwargs[key].get('exclude_from_latest') is not None: + self.body['properties']['publishingProfile']['excludeFromLatest'] = kwargs[key].get('exclude_from_latest') + if kwargs[key].get('end_of_life_date') is not None: + self.body['properties']['publishingProfile']['endOfLifeDate'] = kwargs[key].get('end_of_life_date') + if kwargs[key].get('storage_account_type') is not None: + self.body['properties']['publishingProfile']['storageAccountType'] = kwargs[key].get('storage_account_type') # keep backward compatibility snapshot = self.body.get('properties', {}).get('publishingProfile', {}).pop('snapshot', None) @@ -485,7 +704,6 @@ class AzureRMGalleryImageVersions(AzureRMModuleBaseExt): response = None self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) resource_group = self.get_resource_group(self.resource_group) @@ -531,12 +749,19 @@ class AzureRMGalleryImageVersions(AzureRMModuleBaseExt): self.tags = newtags self.body['tags'] = self.tags self.to_do = Actions.Update - modifiers = {} - self.create_compare_modifiers(self.module_arg_spec, '', modifiers) - self.results['modifiers'] = modifiers - self.results['compare'] = [] - if not self.default_compare(modifiers, self.body, old_response, '', self.results): - self.to_do = Actions.Update + if self.body['properties'].get('publishingProfile') is not None: + for key in self.body['properties']['publishingProfile'].keys(): + if key == 'targetRegions': + result = dict(compare=[]) + modifies = {'/*/name': {'updatable': True, 'comparison': 'location'}} + if not self.default_compare(modifies, self.body['properties']['publishingProfile'][key], + old_response['properties']['publishingProfile'][key], '', result): + self.to_do = Actions.Update + elif key == 'endOfLifeDate': + if self.body['properties']['publishingProfile'][key].lower() != old_response['properties']['publishingProfile'][key].lower(): + self.to_do = Actions.Update + elif self.body['properties']['publishingProfile'].get(key) != old_response['properties']['publishingProfile'].get(key): + self.to_do = Actions.Update if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): self.log('Need to Create / Update the GalleryImageVersion instance') @@ -583,10 +808,12 @@ class AzureRMGalleryImageVersions(AzureRMModuleBaseExt): self.log('Error attempting to create the GalleryImageVersion instance.') self.fail('Error creating the GalleryImageVersion instance: {0}'.format(str(exc))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) while response['properties']['provisioningState'] == 'Creating': time.sleep(60) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimageversion_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimageversion_info.py index b4c7e89a2..2eebb73e7 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimageversion_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_galleryimageversion_info.py @@ -150,7 +150,6 @@ class AzureRMGalleryImageVersionsInfo(AzureRMModuleBase): setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if (self.resource_group is not None and @@ -258,6 +257,8 @@ class AzureRMGalleryImageVersionsInfo(AzureRMModuleBase): return [self.format_item(x) for x in results['response']] if results['response'] else [] def format_item(self, item): + if not item: + return None d = { 'id': item['id'], 'name': item['name'], diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_hdinsightcluster.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_hdinsightcluster.py index 2ce7e16f3..b2121cd27 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_hdinsightcluster.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_hdinsightcluster.py @@ -322,7 +322,6 @@ class AzureRMClusters(AzureRMModuleBase): response = None self.mgmt_client = self.get_mgmt_svc_client(HDInsightManagementClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) resource_group = self.get_resource_group(self.resource_group) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_hdinsightcluster_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_hdinsightcluster_info.py index afb3211ea..b50d51215 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_hdinsightcluster_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_hdinsightcluster_info.py @@ -229,7 +229,6 @@ class AzureRMHDInsightclusterInfo(AzureRMModuleBase): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(HDInsightManagementClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if self.name is not None: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_iotdevice.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_iotdevice.py index be007b8d8..d26062244 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_iotdevice.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_iotdevice.py @@ -378,8 +378,8 @@ class AzureRMIoTDevice(AzureRMModuleBase): elif self.auth_method == 'self_signed': response = self.mgmt_client.update_device_with_certificate_authority(self.name, self.status, iot_edge=self.edge_enabled) elif self.auth_method == 'certificate_authority': - response = self.mgmt_client.update_device_with_x509(self.name, device['etag'], self.primary_thumbprint, - self.secondary_thumbprint, self.status, iot_edge=self.edge_enabled) + response = self.mgmt_client.update_device_with_x509(self.name, device['etag'], self.primary_key, + self.secondary_key, self.status, iot_edge=self.edge_enabled) return self.format_item(response) except Exception as exc: @@ -394,7 +394,7 @@ class AzureRMIoTDevice(AzureRMModuleBase): response = self.mgmt_client.create_device_with_certificate_authority(self.name, self.status, iot_edge=self.edge_enabled) elif self.auth_method == 'certificate_authority': response = self.mgmt_client.create_device_with_x509(self.name, - self.primary_thumbprint, self.secondary_thumbprint, self.status, iot_edge=self.edge_enabled) + self.primary_key, self.secondary_key, self.status, iot_edge=self.edge_enabled) return self.format_item(response) except Exception as exc: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_iotdevicemodule.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_iotdevicemodule.py index 2f2ad679d..274440754 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_iotdevicemodule.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_iotdevicemodule.py @@ -291,11 +291,11 @@ class AzureRMIoTDeviceModule(AzureRMModuleBase): try: if self.auth_method == 'sas': response = self.mgmt_client.update_module_with_sas(self.device, self.name, self.managed_by, self.etag, self.primary_key, self.secondary_key) - elif self.auth_method == 'self_signed': - response = self.mgmt_client.update_module_with_certificate_authority(self.device, self.name, self.managed_by, self.etag) elif self.auth_method == 'certificate_authority': + response = self.mgmt_client.update_module_with_certificate_authority(self.device, self.name, self.managed_by, self.etag) + elif self.auth_method == 'self_signed': response = self.mgmt_client.update_module_with_x509(self.device, - self.name, self.managed_by, self.etag, self.primary_thumbprint, self.secondary_thumbprint) + self.name, self.managed_by, self.etag, self.primary_key, self.secondary_key) return self.format_module(response) except Exception as exc: @@ -309,11 +309,11 @@ class AzureRMIoTDeviceModule(AzureRMModuleBase): try: if self.auth_method == 'sas': response = self.mgmt_client.create_module_with_sas(self.device, self.name, self.managed_by, self.primary_key, self.secondary_key) - elif self.auth_method == 'self_signed': - response = self.mgmt_client.create_module_with_certificate_authority(self.device, self.name, self.managed_by) elif self.auth_method == 'certificate_authority': + response = self.mgmt_client.create_module_with_certificate_authority(self.device, self.name, self.managed_by) + elif self.auth_method == 'self_signed': response = self.mgmt_client.create_module_with_x509(self.device_id, - self.name, self.managed_by, self.primary_thumbprint, self.secondary_thumbprint) + self.name, self.managed_by, self.primary_key, self.secondary_key) return self.format_module(response) except Exception as exc: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvault.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvault.py index efeddaacc..96d2d589e 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvault.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvault.py @@ -382,7 +382,6 @@ class AzureRMVaults(AzureRMModuleBase): self.mgmt_client = self.get_mgmt_svc_client(KeyVaultManagementClient, base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True, api_version="2021-10-01") resource_group = self.get_resource_group(self.resource_group) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvault_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvault_info.py index d7b54515f..af1a245e9 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvault_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvault_info.py @@ -261,7 +261,6 @@ class AzureRMKeyVaultInfo(AzureRMModuleBase): self._client = self.get_mgmt_svc_client(KeyVaultManagementClient, base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True, api_version="2021-10-01") if self.name: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultkey.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultkey.py index 54cc6eff6..d094a4ac4 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultkey.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultkey.py @@ -117,6 +117,7 @@ from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common try: from azure.keyvault.keys import KeyClient + from azure.core.exceptions import ResourceNotFoundError from datetime import datetime except ImportError: # This is handled in azure_rm_common @@ -191,10 +192,12 @@ class AzureRMKeyVaultKey(AzureRMModuleBase): if self.state == 'absent': changed = True - except Exception: + except ResourceNotFoundError as ec: # Key doesn't exist if self.state == 'present': changed = True + except Exception as ec: + self.fail("Find the key vault secret got exception, exception as {0}".format(ec)) self.results['changed'] = changed self.results['state'] = results diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultkey_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultkey_info.py index b59f89b33..979d09f49 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultkey_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultkey_info.py @@ -195,6 +195,7 @@ keyvaults: from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase try: + from azure.core.exceptions import ResourceNotFoundError from azure.keyvault.keys import KeyClient except ImportError: # This is handled in azure_rm_common @@ -282,7 +283,7 @@ def keyitem_to_dict(keyitem): kid=keyitem._id, version=keyitem.version, tags=keyitem._tags, - manged=keyitem._managed, + managed=keyitem._managed, attributes=dict( enabled=keyitem.enabled, not_before=keyitem.not_before, @@ -398,9 +399,10 @@ class AzureRMKeyVaultKeyInfo(AzureRMModuleBase): self.log("Response : {0}".format(response)) results.append(response) - except Exception as e: - self.fail(e) + except ResourceNotFoundError as e: self.log("Did not find the key vault key {0}: {1}".format(self.name, str(e))) + except Exception as ec: + self.fail("Find the key vault key got a exception as {0}".format(ec)) return results def get_key_versions(self): @@ -422,7 +424,7 @@ class AzureRMKeyVaultKeyInfo(AzureRMModuleBase): if self.has_tags(item['tags'], self.tags): results.append(item) except Exception as e: - self.log("Did not find key versions {0} : {1}.".format(self.name, str(e))) + self.fail("Did not find key versions {0} : {1}.".format(self.name, str(e))) return results def list_keys(self): @@ -444,7 +446,7 @@ class AzureRMKeyVaultKeyInfo(AzureRMModuleBase): if self.has_tags(item['tags'], self.tags): results.append(item) except Exception as e: - self.log("Did not find key vault in current subscription {0}.".format(str(e))) + self.fail("Did not find key vault in current subscription {0}.".format(str(e))) return results def get_deleted_key(self): @@ -465,8 +467,11 @@ class AzureRMKeyVaultKeyInfo(AzureRMModuleBase): self.log("Response : {0}".format(response)) results.append(response) - except Exception as e: - self.log("Did not find the key vault key {0}: {1}".format(self.name, str(e))) + except ResourceNotFoundError as ec: + self.log("Did not find the key vault key {0}: {1}".format(self.name, str(ec))) + except Exception as ec: + self.fail("Find the key vault key got a exception {0}".format(str(ec))) + return results def list_deleted_keys(self): @@ -488,7 +493,7 @@ class AzureRMKeyVaultKeyInfo(AzureRMModuleBase): if self.has_tags(item['tags'], self.tags): results.append(item) except Exception as e: - self.log("Did not find key vault in current subscription {0}.".format(str(e))) + self.fail("Did not find key vault in current subscription {0}.".format(str(e))) return results diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultsecret.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultsecret.py index 98a5e0e78..f36c86f1a 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultsecret.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultsecret.py @@ -43,6 +43,14 @@ options: description: - Optional valid-from datetime for secret type: str + recover_if_need: + description: + - Whether to permanently recover delete secrets. + type: bool + purge_if_need: + description: + - Whether to permanently delete secrets. + type: bool state: description: - Assert the state of the subnet. Use C(present) to create or update a secret and C(absent) to delete a secret . @@ -76,6 +84,18 @@ EXAMPLES = ''' secret_name: MySecret keyvault_uri: https://contoso.vault.azure.net/ state: absent + +- name: Recover a delete secret + azure_rm_keyvaultsecret: + secret_name: MySecret + keyvault_uri: https://contoso.vault.azure.net/ + recover_if_need: true + +- name: Purge a delete secret + azure_rm_keyvaultsecret: + secret_name: MySecret + keyvault_uri: https://contoso.vault.azure.net/ + purge_if_need: true ''' RETURN = ''' @@ -96,6 +116,8 @@ from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common try: from azure.keyvault.secrets import SecretClient + from azure.core.exceptions import ResourceNotFoundError + from azure.core.exceptions import HttpResponseError import dateutil.parser except ImportError: # This is handled in azure_rm_common @@ -114,6 +136,8 @@ class AzureRMKeyVaultSecret(AzureRMModuleBase): secret_expiry=dict(type='str', no_log=True), keyvault_uri=dict(type='str', no_log=True, required=True), state=dict(type='str', default='present', choices=['present', 'absent']), + recover_if_need=dict(type='bool'), + purge_if_need=dict(type='bool'), content_type=dict(type='str') ) @@ -135,6 +159,8 @@ class AzureRMKeyVaultSecret(AzureRMModuleBase): self.data_creds = None self.client = None self.tags = None + self.recover_if_need = None + self.purge_if_need = None self.content_type = None super(AzureRMKeyVaultSecret, self).__init__(self.module_arg_spec, @@ -162,10 +188,12 @@ class AzureRMKeyVaultSecret(AzureRMModuleBase): elif self.secret_value and results['secret_value'] != self.secret_value: changed = True - except Exception as ec: + except ResourceNotFoundError as ec: # Secret doesn't exist if self.state == 'present': changed = True + except Exception as ec2: + self.fail("Find the key vault secret got exception, exception as {0}".format(str(ec2))) self.results['changed'] = changed self.results['state'] = results @@ -181,9 +209,21 @@ class AzureRMKeyVaultSecret(AzureRMModuleBase): if not self.check_mode: # Create secret if self.state == 'present' and changed: - results['secret_id'] = self.create_update_secret(self.secret_name, self.secret_value, self.tags, self.content_type, valid_from, expiry) + if self.get_delete_secret(self.secret_name): + if self.recover_if_need: + results['secret_id'] = self.recover_delete_secret(self.secret_name) + status = 'Recover' + elif self.purge_if_need: + self.purge_deleted_secret(self.secret_name) + status = 'Purged' + else: + self.fail("Secret {0} is currently in a deleted but recoverable state, and its name cannot be reused; in this state,\ + the secret can only be recovered or purged.".format(self.secret_name)) + else: + results['secret_id'] = self.create_update_secret(self.secret_name, self.secret_value, self.tags, self.content_type, valid_from, expiry) + status = 'Created' self.results['state'] = results - self.results['state']['status'] = 'Created' + self.results['state']['status'] = status # Delete secret elif self.state == 'absent' and changed: results['secret_id'] = self.delete_secret(self.secret_name) @@ -225,6 +265,31 @@ class AzureRMKeyVaultSecret(AzureRMModuleBase): result = self.get_poller_result(deleted_secret) return result.properties._id + def recover_delete_secret(self, name): + ''' Recover a delete secret ''' + try: + recover_delete_secret = self.client.begin_recover_deleted_secret(name) + result = self.get_poller_result(recover_delete_secret) + return result._id + except HttpResponseError as ec: + self.fail("Recover the delete secret fail, detail info {0}".format(ec)) + + def purge_deleted_secret(self, name): + ''' Purge delete secret ''' + try: + purge_deleted_secret = self.client.purge_deleted_secret(name) + return purge_deleted_secret + except HttpResponseError as ec: + self.fail("Purge delete secret fail, detail info {0}".format(ec)) + + def get_delete_secret(self, name): + ''' Get delete secret ''' + try: + self.client.get_deleted_secret(name=name) + except ResourceNotFoundError: + return False + return True + def main(): AzureRMKeyVaultSecret() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultsecret_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultsecret_info.py index b612dce36..f03977285 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultsecret_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_keyvaultsecret_info.py @@ -163,12 +163,14 @@ from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common try: from azure.keyvault.secrets import SecretClient + from azure.core.exceptions import ResourceNotFoundError except ImportError: # This is handled in azure_rm_common pass def secretbundle_to_dict(bundle): + return dict(tags=bundle._properties._tags, attributes=dict( enabled=bundle._properties._attributes.enabled, @@ -302,9 +304,11 @@ class AzureRMKeyVaultSecretInfo(AzureRMModuleBase): self.log("Response : {0}".format(response)) results.append(response) - except Exception as e: + except ResourceNotFoundError as ec: self.log("Did not find the key vault secret {0}: {1}".format( - self.name, str(e))) + self.name, str(ec))) + except Exception as ec2: + self.fail("Find the key vault secret got exception, exception as {0}".format(str(ec2))) return results def get_secret_versions(self): @@ -326,7 +330,7 @@ class AzureRMKeyVaultSecretInfo(AzureRMModuleBase): if self.has_tags(item['tags'], self.tags): results.append(item) except Exception as e: - self.log("Did not find secret versions {0} : {1}.".format( + self.fail("Did not find secret versions {0} : {1}.".format( self.name, str(e))) return results @@ -349,7 +353,7 @@ class AzureRMKeyVaultSecretInfo(AzureRMModuleBase): if self.has_tags(item['tags'], self.tags): results.append(item) except Exception as e: - self.log( + self.fail( "Did not find key vault in current subscription {0}.".format( str(e))) return results @@ -372,9 +376,12 @@ class AzureRMKeyVaultSecretInfo(AzureRMModuleBase): self.log("Response : {0}".format(response)) results.append(response) - except Exception as e: + except ResourceNotFoundError as ec: self.log("Did not find the key vault secret {0}: {1}".format( - self.name, str(e))) + self.name, str(ec))) + except Exception as ec2: + self.fail("Did not find the key vault secret {0}: {1}".format( + self.name, str(ec2))) return results def list_deleted_secrets(self): @@ -396,7 +403,7 @@ class AzureRMKeyVaultSecretInfo(AzureRMModuleBase): if self.has_tags(item['tags'], self.tags): results.append(item) except Exception as e: - self.log( + self.fail( "Did not find key vault in current subscription {0}.".format( str(e))) return results diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_lock_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_lock_info.py index 39abbf3b7..0805cfa62 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_lock_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_lock_info.py @@ -158,7 +158,7 @@ class AzureRMLockInfo(AzureRMModuleBase): for key in self.module_arg_spec.keys(): setattr(self, key, kwargs[key]) - self._mgmt_client = self.get_mgmt_svc_client(GenericRestClient, is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) + self._mgmt_client = self.get_mgmt_svc_client(GenericRestClient, base_url=self._cloud_environment.endpoints.resource_manager) changed = False # construct scope id scope = self.get_scope() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_manageddisk.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_manageddisk.py index 2b27fbbff..6f6443994 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_manageddisk.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_manageddisk.py @@ -491,12 +491,13 @@ class AzureRMManagedDisk(AzureRMModuleBase): # unmount from the old virtual machine and mount to the new virtual machine if self.managed_by or self.managed_by == '': vm_name = parse_resource_id(disk_instance.get('managed_by', '')).get('name') if disk_instance else None + resource_group = parse_resource_id(disk_instance.get('managed_by', '')).get('resource_group') if disk_instance else None vm_name = vm_name or '' if self.managed_by != vm_name or self.is_attach_caching_option_different(vm_name, result): changed = True if not self.check_mode: if vm_name: - self.detach(self.resource_group, vm_name, result) + self.detach(resource_group, vm_name, result) if self.managed_by: self.attach(self.resource_group, self.managed_by, result) result = self.get_managed_disk() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_managementgroup.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_managementgroup.py index 35235ccbe..f4f03f121 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_managementgroup.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_managementgroup.py @@ -222,17 +222,16 @@ class Actions: class AzureRMManagementGroups(AzureRMModuleBaseExt): def __init__(self): self.module_arg_spec = dict( - group_id=dict(type='str', updatable=False, required=True), - name=dict(type='str', updatable=False), + group_id=dict(type='str', required=True), + name=dict(type='str'), id=dict(type='str'), type=dict(type='str'), properties=dict( type='dict', - disposition="/", options=dict( - tenant_id=dict(type='str', disposition="tenantId"), - display_name=dict(type='str', disposition="displayName"), - parent_id=dict(type='str', disposition="details/parent/id") + tenant_id=dict(type='str'), + display_name=dict(type='str'), + parent_id=dict(type='str') ) ), state=dict(type='str', default='present', choices=['present', 'absent']), @@ -260,16 +259,25 @@ class AzureRMManagementGroups(AzureRMModuleBaseExt): for key in list(self.module_arg_spec.keys()): if hasattr(self, key): setattr(self, key, kwargs[key]) + elif key == 'properties' and kwargs[key] is not None: + self.body['properties'] = {} + for item in kwargs['properties'].keys(): + if item == 'tenant_id': + self.body['properties']['tenantId'] = kwargs['properties'][item] + elif item == 'display_name': + self.body['properties']['displayName'] = kwargs['properties'][item] + elif item == 'parent_id': + self.body['properties']['details'] = {} + self.body['properties']['details']['parent'] = {} + self.body['properties']['details']['parent']['id'] = kwargs['properties'][item] + elif kwargs[key] is not None: self.body[key] = kwargs[key] - self.inflate_parameters(self.module_arg_spec, self.body, 0) - old_response = None response = None self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) self.url = ('/providers' + @@ -293,13 +301,14 @@ class AzureRMManagementGroups(AzureRMModuleBaseExt): if self.state == 'absent': self.to_do = Actions.Delete else: - modifiers = {} - self.results['compare'] = [] - self.create_compare_modifiers(self.module_arg_spec, '', modifiers) - self.results['modifiers'] = modifiers - - if not self.default_compare(modifiers, self.body, old_response, '', self.results): - self.to_do = Actions.Update + for key in self.body.keys(): + if key == 'properties': + if old_response.get('properties') is None or \ + not all(self.body['properties'][item] == old_response['properties'].get(item) + for item in self.body['properties'].keys()): + self.to_do = Actions.Update + elif self.body[key] != old_response.get(key): + self.to_do = Actions.Update if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): self.log('Need to Create / Update the ManagementGroup instance') @@ -346,23 +355,34 @@ class AzureRMManagementGroups(AzureRMModuleBaseExt): # self.log('Creating / Updating the ManagementGroup instance {0}'.format(self.)) try: - response = self.mgmt_client.query(self.url, - 'PUT', - self.query_parameters, - self.header_parameters, - self.body, - self.status_code, - 600, - 30) + if self.to_do == Actions.Create: + response = self.mgmt_client.query(self.url, + 'PUT', + self.query_parameters, + self.header_parameters, + self.body, + self.status_code, + 600, + 30) + else: + response = self.mgmt_client.query(self.url, + 'PATCH', + self.query_parameters, + self.header_parameters, + self.body, + self.status_code, + 600, + 30) except Exception as exc: self.log('Error attempting to create the ManagementGroup instance.') self.fail('Error creating the ManagementGroup instance: {0}'.format(str(exc))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} - pass + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_mysqldatabase.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_mysqldatabase.py index a8caa1745..e26ac2a96 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_mysqldatabase.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_mysqldatabase.py @@ -92,7 +92,7 @@ import time try: from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase - from azure.core.exceptions import ResourceNotFoundError + from azure.core.exceptions import ResourceNotFoundError, HttpResponseError from azure.core.polling import LROPoller except ImportError: # This is handled in azure_rm_common @@ -282,6 +282,8 @@ class AzureRMMySqlDatabase(AzureRMModuleBase): self.log("MySQL Database instance : {0} found".format(response.name)) except ResourceNotFoundError as e: self.log('Did not find the MySQL Database instance.') + except HttpResponseError as e: + self.log("Get MySQL Database instance error. code: {0}, message: {1}".format(e.status_code, str(e.error))) if found is True: return response.as_dict() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_mysqldatabase_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_mysqldatabase_info.py index db2cb8cdf..aef0f1df6 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_mysqldatabase_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_mysqldatabase_info.py @@ -101,7 +101,7 @@ databases: try: from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase - from azure.core.exceptions import ResourceNotFoundError + from azure.core.exceptions import ResourceNotFoundError, HttpResponseError except ImportError: # This is handled in azure_rm_common pass @@ -159,6 +159,8 @@ class AzureRMMySqlDatabaseInfo(AzureRMModuleBase): self.log("Response : {0}".format(response)) except ResourceNotFoundError as e: self.log('Could not get facts for Databases.') + except HttpResponseError as e: + self.log("Get MySQL Database instance error. code: {0}, message: {1}".format(e.status_code, str(e.error))) if response is not None: results.append(self.format_item(response)) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_networkinterface.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_networkinterface.py index bb17132d4..3343d5ac1 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_networkinterface.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_networkinterface.py @@ -816,7 +816,7 @@ class AzureRMNetworkInterface(AzureRMModuleBase): if self.state == 'present': subnet = self.network_models.SubResource( id='/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Network/virtualNetworks/{2}/subnets/{3}'.format( - self.virtual_network['subscription_id'], + self.virtual_network['subscription'] if self.virtual_network.get('subscription') else self.virtual_network['subscription_id'], self.virtual_network['resource_group'], self.virtual_network['name'], self.subnet_name)) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_networkinterface_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_networkinterface_info.py index deeb2c8c6..2a71473d6 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_networkinterface_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_networkinterface_info.py @@ -63,176 +63,214 @@ EXAMPLES = ''' ''' RETURN = ''' -azure_networkinterfaces: - description: - - List of network interface dicts. - returned: always - type: list - example: [{ - "dns_settings": { - "applied_dns_servers": [], - "dns_servers": [], - "internal_dns_name_label": null, - "internal_fqdn": null - }, - "enable_ip_forwarding": false, - "etag": 'W/"59726bfc-08c4-44ed-b900-f6a559876a9d"', - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroup/myResourceGroup/providers/Microsoft.Network/networkInterfaces/nic003", - "ip_configuration": { - "name": "default", - "private_ip_address": "10.10.0.4", - "private_ip_allocation_method": "Dynamic", - "public_ip_address": { - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroup/myResourceGroup/providers/Microsoft.Network/publicIPAddresses/publicip001", - "name": "publicip001" - }, - "subnet": { - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroup/myResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet001/subnets/subnet001", - "name": "subnet001", - "virtual_network_name": "vnet001" - } - }, - "location": "westus", - "mac_address": null, - "name": "nic003", - "network_security_group": { - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroup/myResourceGroup/providers/Microsoft.Network/networkSecurityGroups/secgroup001", - "name": "secgroup001" - }, - "primary": null, - "provisioning_state": "Succeeded", - "tags": {}, - "type": "Microsoft.Network/networkInterfaces" - }] networkinterfaces: description: - List of network interface dicts. Each dict contains parameters can be passed to M(azure.azcollection.azure_rm_networkinterface) module. - type: list + type: complex returned: always contains: id: description: - Id of the network interface. type: str + returned: always + sample: "/subscriptions/xxxx-xxxxx/resourceGroups/testRG/providers/Microsoft.Network/networkInterfaces/nic01" resource_group: description: - Name of a resource group where the network interface exists. type: str + returned: always + sample: testRG name: description: - Name of the network interface. type: str + returned: always + sample: nic01 location: description: - Azure location. type: str + returned: always + sample: eastus virtual_network: description: - An existing virtual network with which the network interface will be associated. - It is a dict which contains I(name) and I(resource_group) of the virtual network. - type: raw + type: complex + returned: always + contains: + name: + description: + - The name of the virtual network relate network interface. + type: str + returned: always + sample: vnetnic01 + resource_gorup: + description: + - Resource groups name that exist on the virtual network. + type: str + returned: always + sample: testRG + subscription_id: + description: + - Virtual network Subscription ID. + type: str + returned: always + sample: xxxxxxx-xxxxxxxxxxxxx + subnet_id: + description: + - The subnet's ID. + type: str + returned: always + sample: "/subscriptions/xxx-xxxx/resourceGroups/testRG/providers/Microsoft.Network/virtualNetworks/nic01/subnets/sub01" subnet: description: - Name of an existing subnet within the specified virtual network. type: str + returned: always + sample: sub01 tags: description: - Tags of the network interface. type: dict + returned: always + sample: {key1: value1, key2: value2} ip_configurations: description: - List of IP configurations, if contains multiple configurations. type: complex + returned: always contains: name: description: - Name of the IP configuration. type: str + returned: always + sample: default private_ip_address: description: - Private IP address for the IP configuration. - type: list + type: str + returned: always + sample: 10.10.0.4 private_ip_allocation_method: description: - Private IP allocation method. type: str + returned: always + sample: Dynamic public_ip_address: description: - Name of the public IP address. None for disable IP address. type: str + returned: always + sample: null public_ip_allocation_method: description: - Public IP allocation method. type: str + returned: always + sample: null load_balancer_backend_address_pools: description: - List of existing load-balancer backend address pools associated with the network interface. type: list + returned: always + sample: null application_gateway_backend_address_pools: description: - List of existing application gateway backend address pools associated with the network interface. version_added: "1.10.0" type: list + returned: always + sample: null primary: description: - Whether the IP configuration is the primary one in the list. type: bool + returned: always + sample: true application_security_groups: description: - List of Application security groups. type: list + returned: always sample: ['/subscriptions/<subsid>/resourceGroups/<rg>/providers/Microsoft.Network/applicationSecurityGroups/myASG'] enable_accelerated_networking: description: - Specifies whether the network interface should be created with the accelerated networking feature or not. type: bool + returned: always + sample: false create_with_security_group: description: - Specifies whether a default security group should be be created with the NIC. Only applies when creating a new NIC. type: bool + returned: always + sample: false security_group: description: - A security group resource ID with which to associate the network interface. type: str + returned: always + sample: null enable_ip_forwarding: description: - Whether to enable IP forwarding type: bool + returned: always + sample: false dns_servers: description: - Which DNS servers should the NIC lookup. - List of IP addresses. type: list + returned: always + sample: [] mac_address: description: - The MAC address of the network interface. type: str + returned: always + sample: null provisioning_state: description: - The provisioning state of the network interface. type: str + returned: always + sample: Succeeded dns_settings: description: - The DNS settings in network interface. type: complex + returned: always contains: dns_servers: description: - List of DNS servers IP addresses. type: list + returned: always + sample: [] applied_dns_servers: description: - If the VM that uses this NIC is part of an Availability Set, then this list will have the union of all DNS servers from all NICs that are part of the Availability Set. This property is what is configured on each of those VMs. type: list + returned: always + sample: [] internal_dns_name_label: description: - Relative DNS name for this NIC used for internal communications between VMs in the same virtual network. type: str + returned: always + sample: null internal_fqdn: description: - Fully qualified DNS name supporting internal communications between VMs in the same virtual network. type: str + returned: always + sample: null ''' # NOQA try: from azure.core.exceptions import ResourceNotFoundError @@ -268,6 +306,8 @@ def nic_to_dict(nic): subnet = subnet_dict.get('subnets') if subnet_dict else None virtual_network = dict( resource_group=subnet_dict.get('resourceGroups'), + subnet_id=config.subnet.id if config and config.subnet else None, + subscription_id=subnet_dict.get('subscriptions'), name=subnet_dict.get('virtualNetworks')) if subnet_dict else None return dict( id=nic.id, diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_openshiftmanagedcluster.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_openshiftmanagedcluster.py index cc579c998..b0c28a190 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_openshiftmanagedcluster.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_openshiftmanagedcluster.py @@ -412,86 +412,57 @@ class AzureRMOpenShiftManagedClusters(AzureRMModuleBaseExt): self.module_arg_spec = dict( resource_group=dict( type='str', - updatable=False, - disposition='resourceGroupName', required=True ), name=dict( type='str', - updatable=False, - disposition='resourceName', required=True ), location=dict( type='str', - updatable=False, - required=True, - disposition='/' + required=True ), cluster_profile=dict( type='dict', - disposition='/properties/clusterProfile', default=dict(), options=dict( pull_secret=dict( type='str', no_log=True, - updatable=False, - disposition='pullSecret', - purgeIfNone=True ), cluster_resource_group_id=dict( type='str', - updatable=False, - disposition='resourceGroupId', - purgeIfNone=True ), domain=dict( type='str', - updatable=False, - disposition='domain', - purgeIfNone=True ), version=dict( type='str', - updatable=False, - disposition='version', - purgeIfNone=True ) ), ), service_principal_profile=dict( type='dict', - disposition='/properties/servicePrincipalProfile', options=dict( client_id=dict( type='str', - updatable=False, - disposition='clientId', required=True ), client_secret=dict( type='str', no_log=True, - updatable=False, - disposition='clientSecret', required=True ) ) ), network_profile=dict( type='dict', - disposition='/properties/networkProfile', options=dict( pod_cidr=dict( type='str', - updatable=False, - disposition='podCidr' ), service_cidr=dict( type='str', - updatable=False, - disposition='serviceCidr' ) ), default=dict( @@ -501,21 +472,15 @@ class AzureRMOpenShiftManagedClusters(AzureRMModuleBaseExt): ), master_profile=dict( type='dict', - disposition='/properties/masterProfile', options=dict( vm_size=dict( type='str', - updatable=False, - disposition='vmSize', choices=['Standard_D8s_v3', 'Standard_D16s_v3', 'Standard_D32s_v3'], - purgeIfNone=True ), subnet_id=dict( type='str', - updatable=False, - disposition='subnetId', required=True ) ) @@ -523,94 +488,66 @@ class AzureRMOpenShiftManagedClusters(AzureRMModuleBaseExt): worker_profiles=dict( type='list', elements='dict', - disposition='/properties/workerProfiles', options=dict( name=dict( type='str', - disposition='name', - updatable=False, required=True, choices=['worker'] ), count=dict( type='int', - disposition='count', - updatable=False, - purgeIfNone=True ), vm_size=dict( type='str', - disposition='vmSize', - updatable=False, choices=['Standard_D4s_v3', 'Standard_D8s_v3'], - purgeIfNone=True ), subnet_id=dict( type='str', - disposition='subnetId', - updatable=False, required=True ), disk_size=dict( type='int', - disposition='diskSizeGB', - updatable=False, - purgeIfNone=True ) ) ), api_server_profile=dict( type='dict', - disposition='/properties/apiserverProfile', options=dict( visibility=dict( type='str', - disposition='visibility', choices=['Public', 'Private'], default='Public' ), url=dict( type='str', - disposition='*', - updatable=False ), ip=dict( type='str', - disposition='*', - updatable=False ) ) ), ingress_profiles=dict( type='list', elements='dict', - disposition='/properties/ingressProfiles', options=dict( name=dict( type='str', - disposition='name', - updatable=False, choices=['default'], default='default' ), visibility=dict( type='str', - disposition='visibility', - updatable=False, choices=['Public', 'Private'], default='Public' ), ip=dict( type='str', - disposition='*', - updatable=False ) ) ), provisioning_state=dict( type='str', - disposition='/properties/provisioningState' ), state=dict( type='str', @@ -630,6 +567,7 @@ class AzureRMOpenShiftManagedClusters(AzureRMModuleBaseExt): self.to_do = Actions.NoAction self.body = {} + self.body['properties'] = {} self.query_parameters = {} self.header_parameters = {} @@ -645,13 +583,58 @@ class AzureRMOpenShiftManagedClusters(AzureRMModuleBaseExt): if hasattr(self, key): setattr(self, key, kwargs[key]) elif kwargs[key] is not None: - self.body[key] = kwargs[key] + if key == 'cluster_profile': + self.body['properties']['clusterProfile'] = {} + for item in ['pull_secret', 'cluster_resource_group_id', 'domain', 'version']: + if item == 'pull_secret': + self.body['properties']['clusterProfile']['pullSecret'] = kwargs[key].get(item) + elif item == 'cluster_resource_group_id': + self.body['properties']['clusterProfile']['resourceGroupId'] = kwargs[key].get(item) + elif item == 'domain': + self.body['properties']['clusterProfile']['domain'] = kwargs[key].get(item) + elif item == 'version': + self.body['properties']['clusterProfile']['version'] = kwargs[key].get(item) + elif key == 'service_principal_profile': + self.body['properties']['servicePrincipalProfile'] = {} + self.body['properties']['servicePrincipalProfile']['ClientId'] = kwargs[key].get('client_id') + self.body['properties']['servicePrincipalProfile']['clientSecret'] = kwargs[key].get('client_secret') + elif key == 'network_profile': + self.body['properties']['networkProfile'] = {} + for item in kwargs[key].keys(): + if item == 'pod_cidr': + self.body['properties']['networkProfile']['podCidr'] = kwargs[key].get(item) + elif item == 'service_cidr': + self.body['properties']['networkProfile']['serviceCidr'] = kwargs[key].get(item) + elif key == 'master_profile': + self.body['properties']['masterProfile'] = {} + if 'subnet_id' in kwargs[key].keys(): + self.body['properties']['masterProfile']['subnetId'] = kwargs[key].get('subnet_id') + self.body['properties']['masterProfile']['vmSize'] = kwargs[key].get('vm_size') + elif key == 'worker_profiles': + self.body['properties']['workerProfiles'] = [] + for item in kwargs[key]: + worker_profile = {} + if item.get('name') is not None: + worker_profile['name'] = item['name'] + if item.get('subnet_id') is not None: + worker_profile['subnetId'] = item['subnet_id'] + worker_profile['count'] = item.get('count') + worker_profile['vmSize'] = item.get('vm_size') + worker_profile['diskSizeGB'] = item.get('disk_size') + + self.body['properties']['workerProfiles'].append(worker_profile) + elif key == 'api_server_profile': + self.body['properties']['apiserverProfile'] = kwargs[key] + elif key == 'ingress_profiles': + self.body['properties']['ingressProfiles'] = kwargs[key] + elif key == 'provisioning_state': + self.body['properties']['provisioningState'] = kwargs[key] + else: + self.body[key] = kwargs[key] - self.inflate_parameters(self.module_arg_spec, self.body, 0) response = None self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) self.url = ('/subscriptions' + @@ -756,11 +739,12 @@ class AzureRMOpenShiftManagedClusters(AzureRMModuleBaseExt): self.log('Error attempting to create the OpenShiftManagedCluster instance.') self.fail('Error creating the OpenShiftManagedCluster instance: {0}' '\n{1}'.format(str(self.body), str(exc))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} - pass + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_openshiftmanagedcluster_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_openshiftmanagedcluster_info.py index ed359c641..cbda2da8b 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_openshiftmanagedcluster_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_openshiftmanagedcluster_info.py @@ -254,7 +254,6 @@ class AzureRMOpenShiftManagedClustersInfo(AzureRMModuleBaseExt): setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if (self.resource_group is not None and self.name is not None): diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_openshiftmanagedclusterkubeconfig_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_openshiftmanagedclusterkubeconfig_info.py new file mode 100644 index 000000000..5b5f73a29 --- /dev/null +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_openshiftmanagedclusterkubeconfig_info.py @@ -0,0 +1,227 @@ +#!/usr/bin/python +# +# Copyright (c) 2020 Haiyuan Zhang <haiyzhan@micosoft.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 + + +DOCUMENTATION = ''' +--- +module: azure_rm_openshiftmanagedclusterkubeconfig_info +version_added: '1.17.0' +short_description: Get admin kubeconfig of Azure Red Hat OpenShift Managed Cluster +description: + - get kubeconfig of Azure Red Hat OpenShift Managed Cluster instance. +options: + resource_group: + description: + - The name of the resource group. + required: true + type: str + name: + description: + - Resource name. + required: true + type: str + path: + description: + - Destination filepath of kubeconfig file + required: false + type: str +extends_documentation_fragment: + - azure.azcollection.azure +author: + - Maxim Babushkin (@maxbab) +''' + +EXAMPLES = ''' +- name: Obtain kubeconfig file of ARO cluster + azure_rm_openshiftmanagedclusterkubeconfig_info: + name: myCluster + resource_group: myResourceGroup + register: kubeconf + +- name: Print registered kubeconfig file + debug: + msg: "{{ kubeconf['kubeconfig'] }}" + +- name: Fetch kubeconfig and save it as mycluster_kubeconfig filename + azure_rm_openshiftmanagedclusterkubeconfig_info: + name: myCluster + resource_group: myResourceGroup + path: ./files/mycluster_kubeconfig + +- name: Fetch kubeconfig and save it to specified directory (file will be named as kubeconfig by default) + azure_rm_openshiftmanagedclusterkubeconfig_info: + name: myCluster + resource_group: myResourceGroup + path: ./files/ +''' + +RETURN = ''' +kubeconfig: + description: + - kubeconfig value + returned: always + type: str +''' + +import base64 +import filecmp +import json +import os +import tempfile +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBaseExt +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_rest import GenericRestClient + + +class Actions: + NoAction, Create, Update, Delete = range(4) + + +class AzureRMOpenShiftManagedClustersKubeconfigInfo(AzureRMModuleBaseExt): + def __init__(self): + self.module_arg_spec = dict( + resource_group=dict( + type='str', required=True + ), + name=dict( + type='str', required=True + ), + path=dict( + type='str', required=False + ) + ) + + self.resource_group = None + self.name = None + self.path = None + + self.results = dict(changed=False) + self.mgmt_client = None + self.state = None + self.url = None + self.status_code = [200] + + self.query_parameters = {} + self.query_parameters['api-version'] = '2021-09-01-preview' + self.header_parameters = {} + self.header_parameters['Content-Type'] = 'application/json; charset=utf-8' + + self.mgmt_client = None + super(AzureRMOpenShiftManagedClustersKubeconfigInfo, self).__init__(self.module_arg_spec, supports_check_mode=True, supports_tags=False) + + def exec_module(self, **kwargs): + + for key in self.module_arg_spec: + setattr(self, key, kwargs[key]) + + self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, is_track2=True, + base_url=self._cloud_environment.endpoints.resource_manager) + self.results = self.get_kubeconfig() + if self.path and self.path_is_valid(): + self.write_kubeconfig_to_file() + return self.results + + def get_kubeconfig(self): + response = None + results = {} + # prepare url + self.url = ('/subscriptions' + + '/{{ subscription_id }}' + + '/resourceGroups' + + '/{{ resource_group }}' + + '/providers' + + '/Microsoft.RedHatOpenShift' + + '/openShiftClusters' + + '/{{ cluster_name }}' + + '/listAdminCredentials') + self.url = self.url.replace('{{ subscription_id }}', self.subscription_id) + self.url = self.url.replace('{{ resource_group }}', self.resource_group) + self.url = self.url.replace('{{ cluster_name }}', self.name) + self.log("Fetch for kubeconfig from the cluster.") + try: + response = self.mgmt_client.query(self.url, + 'POST', + self.query_parameters, + self.header_parameters, + None, + self.status_code, + 600, + 30) + results = json.loads(response.body()) + except Exception as e: + self.log('Could not get info for @(Model.ModuleOperationNameUpper).') + return self.format_item(results) + + def format_item(self, item): + d = { + 'kubeconfig': item.get('kubeconfig'), + } + return d + + def path_is_valid(self): + if not os.path.basename(self.path): + if os.path.isdir(self.path): + self.log("Path is dir. Appending file name.") + self.path += "kubeconfig" + else: + try: + self.log('Attempting to makedirs {0}'.format(self.path)) + os.makedirs(self.path) + except IOError as exc: + self.fail("Failed to create directory {0} - {1}".format(self.path, str(exc))) + self.path += "kubeconfig" + else: + file_name = os.path.basename(self.path) + path = self.path.replace(file_name, '') + self.log('Checking path {0}'. format(path)) + # If the "path" is not defined, it's cwd. + if path and not os.path.isdir(path): + try: + self.log('Attempting to makedirs {0}'. format(path)) + os.makedirs(path) + except IOError as exc: + self.fail("Failed to create directory {0} - {1}".format(path, str(exc))) + self.log("Validated path - {0}". format(self.path)) + return True + + def write_kubeconfig_to_file(self): + decoded_bytes = base64.b64decode(self.results['kubeconfig']) + decoded_string = decoded_bytes.decode("utf-8") + + if os.path.exists(self.path): + self.log('Existing kubeconfig file found. Compare, to decide if needs to override') + # If kubeconfig file already exists, compare it with the new file + # If equal, do nothing, otherwise, override. + tmp_kubeconfig = tempfile.TemporaryFile(mode='w') + tmp_kubeconfig.write(decoded_string) + tmp_kubeconfig.seek(0) + + # No need to close the temp file as it's closed by filecmp.cmp. + if filecmp.cmp(tmp_kubeconfig.name, self.path): + self.log("Files are identical. No need to override.") + self.results['changed'] = False + return + + self.log("Create {0} kubeconfig file.".format(self.path)) + try: + with open(self.path, "w") as file: + file.write(decoded_string) + except Exception as exc: + self.fail("Failed to write kubeconfig output to file - {0} to {1} - {2}".format(self.results['kubeconfig'], + self.path, exc)) + self.log("The {0} kubeconfig file has been created.") + self.results['changed'] = True + return + + +def main(): + AzureRMOpenShiftManagedClustersKubeconfigInfo() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibleconfiguration_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibleconfiguration_info.py new file mode 100644 index 000000000..52810303b --- /dev/null +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibleconfiguration_info.py @@ -0,0 +1,210 @@ +#!/usr/bin/python +# +# Copyright (c) 2024 xuzhang3 (@xuzhang3), Fred-sun (@Fred-sun) +# +# 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 + + +DOCUMENTATION = ''' +--- +module: azure_rm_postgresqlflexibleconfiguration_info +version_added: "2.2.0" +short_description: Get Azure PostgreSQL Flexible Configuration facts +description: + - Get facts of Azure PostgreSQL Flexible Configuration. + +options: + resource_group: + description: + - The name of the resource group that contains the resource. + required: True + type: str + server_name: + description: + - The name of the server. + required: True + type: str + name: + description: + - Setting name. + type: str + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - xuzhang3 (@xuzhang3) + - Fred-sun (@Fred-sun) + +''' + +EXAMPLES = ''' +- name: Get specific setting of PostgreSQL configuration + azure_rm_postgresqlflexibleconfiguration_info: + resource_group: myResourceGroup + server_name: testpostgresqlserver + name: deadlock_timeout + +- name: Get all settings of PostgreSQL Flexible Configuration + azure_rm_postgresqlflexibleconfiguration_info: + resource_group: myResourceGroup + server_name: testpostgresqlserver +''' + +RETURN = ''' +settings: + description: + - A list of dictionaries containing MySQL Server settings. + returned: always + type: complex + contains: + id: + description: + - Setting resource ID. + returned: always + type: str + sample: "/subscriptions/xxx-xxx/resourceGroups/testRG/providers/Microsoft.DBforPostgreSQL/flexibleServers/post2/configurations/xmloption" + name: + description: + - Setting name. + returned: always + type: str + sample: deadlock_timeout + server_name: + description: + - The name of the post gresql flexible server. + type: str + returned: always + sample: post2 + resource_group: + description: + - Name of the server's resource group. + type: str + returned: always + sample: testRG + value: + description: + - Setting value. + returned: always + type: raw + sample: 1000 + description: + description: + - Description of the configuration. + returned: always + type: str + sample: Deadlock timeout. + source: + description: + - Source of the configuration. + returned: always + type: str + sample: system-default +''' + +try: + from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + from azure.core.exceptions import ResourceNotFoundError +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMPostgreSQLFlexibleConfigurationInfo(AzureRMModuleBase): + def __init__(self): + # define user inputs into argument + self.module_arg_spec = dict( + resource_group=dict( + type='str', + required=True + ), + server_name=dict( + type='str', + required=True + ), + name=dict( + type='str' + ) + ) + # store the results of the module operation + self.results = dict( + changed=False + ) + self.resource_group = None + self.server_name = None + self.name = None + super(AzureRMPostgreSQLFlexibleConfigurationInfo, self).__init__(self.module_arg_spec, supports_check_mode=True, supports_tags=False) + + def exec_module(self, **kwargs): + for key in self.module_arg_spec: + setattr(self, key, kwargs[key]) + + if self.name is not None: + self.results['settings'] = self.get() + else: + self.results['settings'] = self.list_by_server() + return self.results + + def get(self): + ''' + Gets facts of the specified PostgreSQL Flexible Configuration. + + :return: deserialized PostgreSQL Flexible Configurationinstance state dictionary + ''' + response = None + try: + response = self.postgresql_flexible_client.configurations.get(resource_group_name=self.resource_group, + server_name=self.server_name, + configuration_name=self.name) + self.log("Response : {0}".format(response)) + except ResourceNotFoundError as e: + self.log('Could not get requested setting, Exception as {0}'.format(e)) + return [] + + return [self.format_item(response)] + + def list_by_server(self): + ''' + Gets facts of the specified PostgreSQL Flexible Configuration. + + :return: deserialized PostgreSQL Flexible Configurationinstance state dictionary + ''' + response = None + results = [] + try: + response = self.postgresql_flexible_client.configurations.list_by_server(resource_group_name=self.resource_group, + server_name=self.server_name) + self.log("Response : {0}".format(response)) + except Exception as e: + self.log('List the flexible server config get exception, except as {0}'.format(e)) + return [] + + if response is not None: + for item in response: + results.append(self.format_item(item)) + + return results + + def format_item(self, item): + d = item.as_dict() + d = { + 'resource_group': self.resource_group, + 'server_name': self.server_name, + 'id': d['id'], + 'name': d['name'], + 'value': d['value'], + 'description': d['description'], + 'source': d['source'] + } + return d + + +def main(): + AzureRMPostgreSQLFlexibleConfigurationInfo() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibledatabase.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibledatabase.py new file mode 100644 index 000000000..0656ddac6 --- /dev/null +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibledatabase.py @@ -0,0 +1,288 @@ +#!/usr/bin/python +# +# Copyright (c) 2024 xuzhang3 (@xuzhang3), Fred-sun (@Fred-sun) +# +# 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 + + +DOCUMENTATION = ''' +--- +module: azure_rm_postgresqlflexibledatabase +version_added: "2.2.0" +short_description: Manage PostgreSQL Flexible Database instance +description: + - Create, update and delete instance of PostgreSQL Flexible Database. + +options: + resource_group: + description: + - The name of the resource group that contains the resource. You can obtain this value from the Azure Resource Manager API or the portal. + required: True + type: str + server_name: + description: + - The name of the server. + required: True + type: str + name: + description: + - The name of the database. + required: True + type: str + charset: + description: + - The charset of the database. + type: str + collation: + description: + - The collation of the database. + type: str + state: + description: + - Assert the state of the PostgreSQL Flexible database. Use C(present) to create or update a database and C(absent) to delete it. + default: present + type: str + choices: + - absent + - present + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - xuzhang3 (@xuzhang3) + - Fred-sun (@Fred-sun) + +''' + +EXAMPLES = ''' +- name: Create (or update) PostgreSQL Flexible Database + azure_rm_postgresqlflexibledatabase: + resource_group: myResourceGroup + server_name: testserver + name: db1 + charset: UTF8 + collation: en_US.utf8 + +- name: Delete PostgreSQL Flexible Database + azure_rm_postgresqlflexibledatabase: + resource_group: myResourceGroup + server_name: testserver + name: db1 +''' + +RETURN = ''' +database: + description: + - A list of dictionaries containing facts for PostgreSQL Flexible Database. + returned: always + type: complex + contains: + id: + description: + - Resource ID of the postgresql flexible database. + returned: always + type: str + sample: "/subscriptions/xxx-xxx/resourceGroups/testRG/providers/Microsoft.DBforPostgreSQL/flexibleServers/postfle9/databases/freddatabase" + name: + description: + - Resource name. + returned: always + type: str + sample: freddatabase + charset: + description: + - The charset of the database. + returned: always + type: str + sample: UTF-8 + collation: + description: + - The collation of the database. + returned: always + type: str + sample: en_US.utf8 + type: + description: + - The type of the resource. + returned: always + type: str + sample: Microsoft.DBforPostgreSQL/flexibleServers/databases +''' + + +try: + from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + from azure.core.exceptions import ResourceNotFoundError + from azure.core.polling import LROPoller +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMPostgreSqlFlexibleDatabases(AzureRMModuleBase): + """Configuration class for an Azure RM PostgreSQL Flexible Database resource""" + + def __init__(self): + self.module_arg_spec = dict( + resource_group=dict( + type='str', + required=True + ), + server_name=dict( + type='str', + required=True + ), + name=dict( + type='str', + required=True + ), + charset=dict( + type='str' + ), + collation=dict( + type='str' + ), + state=dict( + type='str', + default='present', + choices=['present', 'absent'] + ) + ) + + self.resource_group = None + self.server_name = None + self.name = None + self.parameters = dict() + + self.results = dict(changed=False) + self.state = None + + super(AzureRMPostgreSqlFlexibleDatabases, self).__init__(derived_arg_spec=self.module_arg_spec, + supports_check_mode=True, + supports_tags=False) + + def exec_module(self, **kwargs): + """Main module execution method""" + + for key in list(self.module_arg_spec.keys()): + if hasattr(self, key): + setattr(self, key, kwargs[key]) + elif kwargs[key] is not None: + if key == "charset": + self.parameters["charset"] = kwargs[key] + elif key == "collation": + self.parameters["collation"] = kwargs[key] + + old_response = None + response = None + changed = False + + old_response = self.get_postgresqlflexibledatabase() + + if not old_response: + self.log("PostgreSQL Flexible Database instance doesn't exist") + if self.state == 'absent': + self.log("Old instance didn't exist") + else: + changed = True + if not self.check_mode: + response = self.create_update_postgresqlflexibledatabase(self.parameters) + else: + self.log("PostgreSQL Flexible Database instance already exists") + if self.state == 'absent': + changed = True + if not self.check_mode: + response = self.delete_postgresqlflexibledatabase() + else: + if (self.parameters.get('charset') is not None and self.parameters['charset'] != old_response['charset']) or\ + (self.parameters.get('collation') is not None and self.parameters['collation'] != old_response['collation']): + changed = True + if not self.check_mode: + self.fail("The Post Gresql Flexible database not support to update") + else: + response = old_response + + self.results['database'] = response + self.results['changed'] = changed + return self.results + + def create_update_postgresqlflexibledatabase(self, body): + ''' + Creates or updates PostgreSQL Flexible Database with the specified configuration. + + :return: deserialized PostgreSQL Flexible Database instance state dictionary + ''' + self.log("Creating / Updating the PostgreSQL Flexible Database instance {0}".format(self.name)) + + try: + response = self.postgresql_flexible_client.databases.begin_create(resource_group_name=self.resource_group, + server_name=self.server_name, + database_name=self.name, + parameters=body) + if isinstance(response, LROPoller): + response = self.get_poller_result(response) + + except Exception as exc: + self.log('Error attempting to create the PostgreSQL Flexible Database instance.') + self.fail("Error creating the PostgreSQL Flexible Database instance: {0}".format(str(exc))) + return self.format_item(response) + + def delete_postgresqlflexibledatabase(self): + ''' + Deletes specified PostgreSQL Flexible Database instance in the specified subscription and resource group. + + :return: True + ''' + self.log("Deleting the PostgreSQL Flexible Database instance {0}".format(self.name)) + try: + self.postgresql_flexible_client.databases.begin_delete(resource_group_name=self.resource_group, + server_name=self.server_name, + database_name=self.name) + except Exception as ec: + self.log('Error attempting to delete the PostgreSQL Flexible Database instance.') + self.fail("Error deleting the PostgreSQL Flexible Database instance: {0}".format(str(ec))) + + def get_postgresqlflexibledatabase(self): + ''' + Gets the properties of the specified PostgreSQL Flexible Database. + + :return: deserialized PostgreSQL Flexible Database instance state dictionary + ''' + self.log("Checking if the PostgreSQL Flexible Database instance {0} is present".format(self.name)) + found = False + try: + response = self.postgresql_flexible_client.databases.get(resource_group_name=self.resource_group, + server_name=self.server_name, + database_name=self.name) + found = True + self.log("Response : {0}".format(response)) + self.log("PostgreSQL Flexible Database instance : {0} found".format(response.name)) + except ResourceNotFoundError as e: + self.log('Did not find the PostgreSQL Flexible Database instance. Exception as {0}'.format(e)) + if found is True: + return self.format_item(response) + + return None + + def format_item(self, item): + result = dict( + id=item.id, + name=item.name, + type=item.type, + charset=item.charset, + collation=item.collation + ) + return result + + +def main(): + """Main execution""" + AzureRMPostgreSqlFlexibleDatabases() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibledatabase_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibledatabase_info.py new file mode 100644 index 000000000..545baf53f --- /dev/null +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibledatabase_info.py @@ -0,0 +1,239 @@ +#!/usr/bin/python +# +# Copyright (c) 2024 xuzhang3 (@xuzhang3), Fred-sun (@Fred-sun) +# +# 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 + + +DOCUMENTATION = ''' +--- +module: azure_rm_postgresqlflexibledatabase_info +version_added: "2.2.0" +short_description: Get Azure PostgreSQL Flexible Database facts +description: + - Get facts of PostgreSQL Flexible Database. + +options: + resource_group: + description: + - The name of the resource group that contains the resource. You can obtain this value from the Azure Resource Manager API or the portal. + type: str + required: True + server_name: + description: + - The name of the post gresql server. + type: str + required: True + name: + description: + - The name of the post gresql database. + type: str + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - xuzhang3 (@xuzhang3) + - Fred-sun (@Fred-sun) + +''' + +EXAMPLES = ''' +- name: List instance of PostgreSQL Flexible Database by server name + azure_rm_postgresqlflexibledatabase_info: + resource_group: myResourceGroup + server_name: server_name + +- name: Get instances of PostgreSQL Flexible Database + azure_rm_postgresqlflexibledatabase_info: + resource_group: myResourceGroup + server_name: server_name + name: database_name +''' + +RETURN = ''' +database: + description: + - A list of dictionaries containing facts for PostgreSQL Flexible Database. + returned: always + type: complex + contains: + id: + description: + - Resource ID of the postgresql flexible database. + returned: always + type: str + sample: "/subscriptions/xxx-xxx/resourceGroups/testRG/providers/Microsoft.DBforPostgreSQL/flexibleServers/postfle9/databases/freddatabase" + name: + description: + - Resource name. + returned: always + type: str + sample: freddatabase + charset: + description: + - The charset of the database. + returned: always + type: str + sample: UTF-8 + collation: + description: + - The collation of the database. + returned: always + type: str + sample: en_US.utf8 + type: + description: + - The type of the resource. + returned: always + type: str + sample: Microsoft.DBforPostgreSQL/flexibleServers/databases + system_data: + description: + - The system metadata relating to this resource. + type: complex + returned: always + contains: + created_by: + description: + - The identity that created the resource. + type: str + returned: always + sample: null + created_by_type: + description: + - The type of identity that created the resource. + returned: always + type: str + sample: null + created_at: + description: + - The timestamp of resource creation (UTC). + returned: always + sample: null + type: str + last_modified_by: + description: + - The identity that last modified the resource. + type: str + returned: always + sample: null + last_modified_by_type: + description: + - The type of identity that last modified the resource. + returned: always + sample: null + type: str + last_modified_at: + description: + - The timestamp of resource last modification (UTC). + returned: always + sample: null + type: str +''' + + +try: + from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + from azure.core.exceptions import ResourceNotFoundError +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMPostgreSqlFlexibleDatabaseInfo(AzureRMModuleBase): + def __init__(self): + # define user inputs into argument + self.module_arg_spec = dict( + resource_group=dict( + type='str', + required=True + ), + server_name=dict( + type='str', + required=True + ), + name=dict( + type='str' + ), + ) + # store the results of the module operation + self.results = dict( + changed=False + ) + self.resource_group = None + self.name = None + self.server_name = None + super(AzureRMPostgreSqlFlexibleDatabaseInfo, self).__init__(self.module_arg_spec, supports_check_mode=True, 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 is not None: + self.results['databases'] = self.get() + else: + self.results['databases'] = self.list_all() + return self.results + + def get(self): + response = None + results = [] + try: + response = self.postgresql_flexible_client.databases.get(resource_group_name=self.resource_group, + server_name=self.server_name, + database_name=self.name) + self.log("Response : {0}".format(response)) + except ResourceNotFoundError: + self.log('Could not get facts for PostgreSQL Flexible Server.') + + if response is not None: + results.append(self.format_item(response)) + + return results + + def list_all(self): + response = None + results = [] + try: + response = self.postgresql_flexible_client.databases.list_by_server(resource_group_name=self.resource_group, + server_name=self.server_name) + self.log("Response : {0}".format(response)) + except Exception as ec: + self.log('Could not get facts for PostgreSQL Flexible Servers.') + + if response is not None: + for item in response: + results.append(self.format_item(item)) + + return results + + def format_item(self, item): + result = dict( + id=item.id, + name=item.name, + system_data=dict(), + type=item.type, + charset=item.charset, + collation=item.collation + ) + if item.system_data is not None: + result['system_data']['created_by'] = item.system_data.created_by + result['system_data']['created_by_type'] = item.system_data.created_by_type + result['system_data']['created_at'] = item.system_data.created_at + result['system_data']['last_modified_by'] = item.system_data.last_modified_by + result['system_data']['last_modified_by_type'] = item.system_data.last_modified_by_type + result['system_data']['last_modified_at'] = item.system_data.last_modified_at + + return result + + +def main(): + AzureRMPostgreSqlFlexibleDatabaseInfo() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexiblefirewallrule.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexiblefirewallrule.py new file mode 100644 index 000000000..c73843c46 --- /dev/null +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexiblefirewallrule.py @@ -0,0 +1,294 @@ +#!/usr/bin/python +# +# Copyright (c) 2024 xuzhang3 (@xuzhang3), Fred-sun (@Fred-sun) +# +# 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 + + +DOCUMENTATION = ''' +--- +module: azure_rm_postgresqlflexiblefirewallrule +version_added: "2.2.0" +short_description: Manage PostgreSQL flexible firewall rule instance +description: + - Create, update and delete instance of PostgreSQL flexible firewall rule. + +options: + resource_group: + description: + - The name of the resource group that contains the resource. You can obtain this value from the Azure Resource Manager API or the portal. + required: True + type: str + server_name: + description: + - The name of the server. + required: True + type: str + name: + description: + - The name of the PostgreSQL flexible firewall rule. + required: True + type: str + start_ip_address: + description: + - The start IP address of the PostgreSQL flexible firewall rule. Must be IPv4 format. + type: str + end_ip_address: + description: + - The end IP address of the PostgreSQL flexible firewall rule. Must be IPv4 format. + type: str + state: + description: + - Assert the state of the PostgreSQL flexible firewall rule. + - Use C(present) to create or update a PostgreSQL flexible firewall rule and C(absent) to delete it. + default: present + type: str + choices: + - absent + - present + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - xuzhang3 (@xuzhang3) + - Fred-sun (@Fred-sun) + +''' + +EXAMPLES = ''' +- name: Create (or update) PostgreSQL flexible firewall rule + azure_rm_postgresqlflexiblefirewallrule: + resource_group: myResourceGroup + server_name: testserver + name: rule1 + start_ip_address: 10.0.0.16 + end_ip_address: 10.0.0.18 +''' + +RETURN = ''' +rules: + description: + - A list of dictionaries containing facts for PostgreSQL Flexible Firewall Rule. + returned: always + type: complex + contains: + id: + description: + - Resource ID. + returned: always + type: str + sample: "/subscriptions/xxx-xxx/resourceGroups/testRG/providers/Microsoft.DBforPostgreSQL/flexibleServers/flexibled9b/firewallRules/firewalld9b" + server_name: + description: + - The name of the server. + returned: always + type: str + sample: testserver + name: + description: + - Resource name. + returned: always + type: str + sample: rule1 + start_ip_address: + description: + - The start IP address of the PostgreSQL firewall rule. + returned: always + type: str + sample: 10.0.0.16 + end_ip_address: + description: + - The end IP address of the PostgreSQL firewall rule. + returned: always + type: str + sample: 10.0.0.18 +''' + + +try: + from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + from azure.core.exceptions import ResourceNotFoundError + from azure.core.polling import LROPoller + import logging + logging.basicConfig(filename='log.log', level=logging.INFO) +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMPostgreSqlFlexibleFirewallRules(AzureRMModuleBase): + """Configuration class for an Azure RM PostgreSQL flexible firewall rule resource""" + + def __init__(self): + self.module_arg_spec = dict( + resource_group=dict( + type='str', + required=True + ), + server_name=dict( + type='str', + required=True + ), + name=dict( + type='str', + required=True + ), + start_ip_address=dict( + type='str' + ), + end_ip_address=dict( + type='str' + ), + state=dict( + type='str', + default='present', + choices=['present', 'absent'] + ) + ) + + self.resource_group = None + self.server_name = None + self.name = None + self.start_ip_address = None + self.end_ip_address = None + + self.results = dict(changed=False) + self.state = None + self.parameters = dict() + + super(AzureRMPostgreSqlFlexibleFirewallRules, self).__init__(derived_arg_spec=self.module_arg_spec, + supports_check_mode=True, + supports_tags=False) + + def exec_module(self, **kwargs): + """Main module execution method""" + for key in list(self.module_arg_spec.keys()): + if hasattr(self, key): + setattr(self, key, kwargs[key]) + if key in ['start_ip_address', 'end_ip_address']: + self.parameters[key] = kwargs[key] + + old_response = None + response = None + changed = False + + old_response = self.get_firewallrule() + + if old_response is None: + self.log("PostgreSQL flexible firewall rule instance doesn't exist") + if self.state == 'absent': + self.log("Old instance didn't exist") + else: + changed = True + if not self.check_mode: + response = self.create_update_firewallrule(self.parameters) + else: + self.log("PostgreSQL flexible firewall rule instance already exists") + if self.state == 'absent': + changed = True + if self.check_mode: + response = old_response + else: + response = self.delete_firewallrule() + else: + self.log("Need to check if PostgreSQL flexible firewall rule instance has to be deleted or may be updated") + if (self.start_ip_address is not None) and (self.start_ip_address != old_response['start_ip_address']): + changed = True + else: + self.parameters['start_ip_address'] = old_response['start_ip_address'] + if (self.end_ip_address is not None) and (self.end_ip_address != old_response['end_ip_address']): + changed = True + else: + self.parameters['end_ip_address'] = old_response['end_ip_address'] + if changed: + if not self.check_mode: + response = self.create_update_firewallrule(self.parameters) + else: + response = old_response + else: + response = old_response + self.results['firewall_rule'] = response + self.results['changed'] = changed + + return self.results + + def create_update_firewallrule(self, body): + ''' + Creates or updates PostgreSQL flexible firewall rule with the specified configuration. + + :return: deserialized PostgreSQL flexible firewall rule instance state dictionary + ''' + self.log("Creating / Updating the PostgreSQL flexible firewall rule instance {0}".format(self.name)) + + try: + response = self.postgresql_flexible_client.firewall_rules.begin_create_or_update(resource_group_name=self.resource_group, + server_name=self.server_name, + firewall_rule_name=self.name, + parameters=body) + if isinstance(response, LROPoller): + response = self.get_poller_result(response) + + except Exception as exc: + self.log('Error attempting to create the PostgreSQL flexible firewall rule instance.') + self.fail("Error creating the PostgreSQL flexible firewall rule instance: {0}".format(str(exc))) + return self.format_item(response) + + def delete_firewallrule(self): + ''' + Deletes specified PostgreSQL flexible firewall rule instance in the specified subscription and resource group. + + :return: True + ''' + self.log("Deleting the PostgreSQL flexible firewall rule instance {0}".format(self.name)) + try: + self.postgresql_flexible_client.firewall_rules.begin_delete(resource_group_name=self.resource_group, + server_name=self.server_name, + firewall_rule_name=self.name) + except Exception as e: + self.log('Error attempting to delete the PostgreSQL flexible firewall rule instance.') + self.fail("Error deleting the PostgreSQL flexible firewall rule instance: {0}".format(str(e))) + + return True + + def get_firewallrule(self): + ''' + Gets the properties of the specified PostgreSQL flexible firewall rule. + + :return: deserialized PostgreSQL flexible firewall rule instance state dictionary + ''' + self.log("Checking if the PostgreSQL flexible firewall rule instance {0} is present".format(self.name)) + try: + response = self.postgresql_flexible_client.firewall_rules.get(resource_group_name=self.resource_group, + server_name=self.server_name, + firewall_rule_name=self.name) + self.log("Response : {0}".format(response)) + self.log("PostgreSQL flexible firewall rule instance : {0} found".format(response.name)) + except ResourceNotFoundError as e: + self.log('Did not find the PostgreSQL flexible firewall rule instance. Exception as {0}'.format(str(e))) + return None + return self.format_item(response) + + def format_item(self, item): + d = item.as_dict() + d = { + 'resource_group': self.resource_group, + 'id': d['id'], + 'server_name': self.server_name, + 'name': d['name'], + 'start_ip_address': d['start_ip_address'], + 'end_ip_address': d['end_ip_address'] + } + return d + + +def main(): + """Main execution""" + AzureRMPostgreSqlFlexibleFirewallRules() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexiblefirewallrule_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexiblefirewallrule_info.py new file mode 100644 index 000000000..14eb029b4 --- /dev/null +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexiblefirewallrule_info.py @@ -0,0 +1,187 @@ +#!/usr/bin/python +# +# Copyright (c) 2024 xuzhang3 (@xuzhang3), Fred-sun (@Fred-sun) +# +# 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 + + +DOCUMENTATION = ''' +--- +module: azure_rm_postgresqlflexiblefirewallrule_info +version_added: "2.2.0" +short_description: Get Azure PostgreSQL Flexible Firewall Rule facts +description: + - Get facts of Azure PostgreSQL Flexible Firewall Rule. + +options: + resource_group: + description: + - The name of the resource group. + required: True + type: str + server_name: + description: + - The name of the server. + required: True + type: str + name: + description: + - The name of the server firewall rule. + type: str + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - xuzhang3 (@xuzhang3) + - Fred-sun (@Fred-sun) + +''' + +EXAMPLES = ''' +- name: Get instance of PostgreSQL Flexible Firewall Rule + azure_rm_postgresqlflexiblefirewallrule_info: + resource_group: myResourceGroup + server_name: server_name + name: firewall_rule_name + +- name: List instances of PostgreSQL Flexible Firewall Rule + azure_rm_postgresqlflexiblefirewallrule_info: + resource_group: myResourceGroup + server_name: server_name +''' + +RETURN = ''' +rules: + description: + - A list of dictionaries containing facts for PostgreSQL Flexible Firewall Rule. + returned: always + type: complex + contains: + id: + description: + - Resource ID. + returned: always + type: str + sample: "/subscriptions/xxx-xxx/resourceGroups/testRG/providers/Microsoft.DBforPostgreSQL/flexibleServers/flexibled9b/firewallRules/firewalld9b" + server_name: + description: + - The name of the server. + returned: always + type: str + sample: testserver + name: + description: + - Resource name. + returned: always + type: str + sample: rule1 + start_ip_address: + description: + - The start IP address of the PostgreSQL firewall rule. + returned: always + type: str + sample: 10.0.0.16 + end_ip_address: + description: + - The end IP address of the PostgreSQL firewall rule. + returned: always + type: str + sample: 10.0.0.18 +''' + +try: + from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + from azure.core.exceptions import ResourceNotFoundError +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMPostgreSQLFlexibleFirewallRulesInfo(AzureRMModuleBase): + def __init__(self): + # define user inputs into argument + self.module_arg_spec = dict( + resource_group=dict( + type='str', + required=True + ), + server_name=dict( + type='str', + required=True + ), + name=dict( + type='str' + ) + ) + # store the results of the module operation + self.results = dict( + changed=False + ) + self.resource_group = None + self.server_name = None + self.name = None + super(AzureRMPostgreSQLFlexibleFirewallRulesInfo, self).__init__(self.module_arg_spec, supports_check_mode=True, supports_tags=False) + + def exec_module(self, **kwargs): + for key in self.module_arg_spec: + setattr(self, key, kwargs[key]) + + if self.name is not None: + self.results['firewall_rules'] = self.get() + else: + self.results['firewall_rules'] = self.list_by_server() + return self.results + + def get(self): + response = None + try: + response = self.postgresql_flexible_client.firewall_rules.get(resource_group_name=self.resource_group, + server_name=self.server_name, + firewall_rule_name=self.name) + self.log("Response : {0}".format(response)) + except ResourceNotFoundError as e: + self.log('Could not get facts for FirewallRules. Exception as {0}'.format(str(e))) + return [] + + return [self.format_item(response)] + + def list_by_server(self): + response = None + results = [] + try: + response = self.postgresql_flexible_client.firewall_rules.list_by_server(resource_group_name=self.resource_group, + server_name=self.server_name) + self.log("Response : {0}".format(response)) + except Exception as e: + self.log('Could not get facts for FirewallRules. Exception as {0}'.format(str(e))) + return [] + + if response is not None: + for item in response: + results.append(self.format_item(item)) + + return results + + def format_item(self, item): + d = item.as_dict() + d = { + 'resource_group': self.resource_group, + 'id': d['id'], + 'server_name': self.server_name, + 'name': d['name'], + 'start_ip_address': d['start_ip_address'], + 'end_ip_address': d['end_ip_address'] + } + return d + + +def main(): + AzureRMPostgreSQLFlexibleFirewallRulesInfo() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibleserver.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibleserver.py new file mode 100644 index 000000000..335dc53c8 --- /dev/null +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibleserver.py @@ -0,0 +1,928 @@ +#!/usr/bin/python +# +# Copyright (c) 2024 xuzhang3 (@xuzhang3), Fred-sun (@Fred-sun) +# +# 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 + + +DOCUMENTATION = ''' +--- +module: azure_rm_postgresqlflexibleserver +version_added: "2.2.0" +short_description: Manage PostgreSQL Flexible Server instance +description: + - Create, update and delete instance of PostgreSQL Flexible Server. + +options: + resource_group: + description: + - The name of the resource group that contains the resource. + - You can obtain this value from the Azure Resource Manager API or the portal. + required: True + type: str + name: + description: + - The name of the flexible server. + required: True + type: str + sku: + description: + - The SKU (pricing tier) of the server. + type: dict + suboptions: + name: + description: + - The name of the sku, typically, tier + family + cores, such as Standard_D4s_v3. + type: str + required: True + tier: + description: + - The tier of the particular + type: str + choices: + - Burstable + - GeneralPurpose + - MemoryOptimized + required: True + location: + description: + - Resource location. If not set, location from the resource group will be used as default. + type: str + storage: + description: + - Storage properties of a server. + type: dict + suboptions: + storage_size_gb: + description: + - The storage size for the server. + type: int + administrator_login: + description: + - The administrator's login name of a server. + - Can only be specified when the server is being created (and is required for creation). + type: str + administrator_login_password: + description: + - The administrator login password (required for server creation). + type: str + version: + description: + - PostgreSQL Server version. + type: str + choices: + - '11' + - '12' + - '13' + fully_qualified_domain_name: + description: + - The fully qualified domain name of a server. + type: str + backup: + description: + - Backup properties of a server. + type: dict + suboptions: + backup_retention_days: + description: + - Backup retention days for the server. + type: int + geo_redundant_backup: + description: + - A value indicating whether Geo-Redundant backup is enabled on the server. + type: str + choices: + - Enabled + - Disabled + network: + description: + - Network properties of a server. + type: dict + suboptions: + delegated_subnet_resource_id: + description: + - Delegated subnet arm resource id. + type: str + private_dns_zone_arm_resource_id: + description: + - Private dns zone arm resource id. + type: str + public_network_access: + description: + - Public network access is enabled or not. + type: str + choices: + - Enabled + - Disabled + high_availability: + description: + - High availability properties of a server. + type: dict + suboptions: + mode: + description: + - The HA mode for the server. + type: str + choices: + - Disabled + - ZoneRedundant + standby_availability_zone: + description: + - Availability zone information of the standby. + type: str + maintenance_window: + description: + - Maintenance window properties of a server. + type: dict + suboptions: + custom_window: + description: + - Indicates whether custom window is enabled or disabled. + type: str + start_hour: + description: + - Start hour for maintenance window. + type: int + start_minute: + description: + - Start minute for maintenance window. + type: int + day_of_week: + description: + - Day of week for maintenance window. + type: int + point_in_time_utc: + description: + - Restore point creation time (ISO8601 format), specifying the time to restore from. + - It's required when I(create_mode=PointInTimeRestore). + type: str + availability_zone: + description: + - Availability zone information of the server + type: str + create_mode: + description: + - The mode to create a new PostgreSQL server. + type: str + choices: + - Default + - Create + - Update + - PointInTimeRestore + source_server_resource_id: + description: + - The source server resource ID to restore from. + - It's required when I(create_mode=PointInTimeRestore) + type: str + state: + description: + - Assert the state of the PostgreSQL Flexible server. + - Use C(present) to create or update a server and C(absent) to delete it. + default: present + type: str + choices: + - present + - absent + is_restart: + description: + - Whether to restart the Post gresql server. + type: bool + default: False + is_stop: + description: + - Whether to stop the Post gresql server. + type: bool + default: False + is_start: + description: + - Whether to start the Post gresql server. + type: bool + default: False + +extends_documentation_fragment: + - azure.azcollection.azure + - azure.azcollection.azure_tags + +author: + - xuzhang3 (@xuzhang3) + - Fred-sun (@Fred-sun) + +''' + +EXAMPLES = ''' +- name: Create (or update) PostgreSQL Flexible Server + azure_rm_postgresqlflexibleserver: + resource_group: myResourceGroup + name: testserver + sku: + name: Standard_B1ms + tier: Burstable + administrator_login: azureuser + administrator_login_password: Fred@0329 + version: 12 + storage: + storage_size_gb: 128 + fully_qualified_domain_name: st-private-dns-zone.postgres.database.azure.com + backup: + backup_retention_days: 7 + geo_redundant_backup: Disabled + maintenance_window: + custom_window: Enabled + start_hour: 8 + start_minute: 0 + day_of_week: 0 + point_in_time_utc: 2023-05-31T00:28:17.7279547+00:00 + availability_zone: 1 + create_mode: Default + +- name: Delete PostgreSQL Flexible Server + azure_rm_postgresqlflexibleserver: + resource_group: myResourceGroup + name: testserver + state: absent +''' + +RETURN = ''' +servers: + description: + - A list of dictionaries containing facts for PostgreSQL Flexible servers. + returned: always + type: complex + contains: + id: + description: + - Resource ID of the postgresql flexible server. + returned: always + type: str + sample: "/subscriptions/xxx/resourceGroups/myResourceGroup/providers/Microsoft.DBforPostgreSQL/flexibleservers/postgresql3" + resource_group: + description: + - Resource group name. + returned: always + type: str + sample: myResourceGroup + name: + description: + - Resource name. + returned: always + type: str + sample: postgreabdud1223 + location: + description: + - The location the resource resides in. + returned: always + type: str + sample: eastus + sku: + description: + - The SKU of the server. + returned: always + type: complex + contains: + name: + description: + - The name of the SKU. + returned: always + type: str + sample: Standard_B1ms + tier: + description: + - The tier of the particular SKU. + returned: always + type: str + sample: Burstable + storage: + description: + - The maximum storage allowed for a server. + returned: always + type: complex + contains: + storage_size_gb: + description: + - ax storage allowed for a server. + type: int + returned: always + sample: 128 + administrator_login: + description: + - The administrator's login name of a server. + returned: always + type: str + sample: azureuser + version: + description: + - Flexible Server version. + returned: always + type: str + sample: "12" + choices: + - '11' + - '12' + - '13' + fully_qualified_domain_name: + description: + - The fully qualified domain name of the flexible server. + returned: always + type: str + sample: postflexiblefredpgsqlflexible.postgres.database.azure.com + availability_zone: + description: + - Availability zone information of the server. + type: str + returned: always + sample: 1 + backup: + description: + - Backup properties of a server. + type: complex + returned: always + contains: + backup_retention_days: + description: + - Backup retention days for the server. + type: int + returned: always + sample: 7 + geo_redundant_backup: + description: + - A value indicating whether Geo-Redundant backup is enabled on the server. + type: str + returned: always + sample: Disabled + high_availability: + description: + - High availability properties of a server. + type: complex + returned: always + contains: + mode: + description: + - The HA mode for the server. + returned: always + sample: Disabled + type: str + standby_availability_zone: + description: + - availability zone information of the standby. + type: str + returned: always + sample: null + maintenance_window: + description: + - Maintenance window properties of a server. + type: complex + returned: always + contains: + custom_window: + description: + - Indicates whether custom window is enabled or disabled. + returned: always + sample: Enabled + type: str + day_of_week: + description: + - Day of week for maintenance window. + returned: always + sample: 0 + type: int + start_hour: + description: + - Start hour for maintenance window. + type: int + returned: always + sample: 8 + start_minute: + description: + - Start minute for maintenance window. + type: int + returned: always + sample: 0 + network: + description: + - Network properties of a server. + type: complex + returned: always + contains: + delegated_subnet_resource_id: + description: + - Delegated subnet arm resource id. + type: str + returned: always + sample: null + private_dns_zone_arm_resource_id: + description: + - Private dns zone arm resource id. + type: str + returned: always + sample: null + public_network_access: + description: + - Public network access is enabled or not. + type: str + returned: always + sample: Enabled + point_in_time_utc: + description: + - Restore point creation time (ISO8601 format). + type: str + sample: null + returned: always + source_server_resource_id: + description: + - The source server resource ID to restore from. + type: str + returned: always + sample: null + system_data: + description: + - The system metadata relating to this resource. + type: complex + returned: always + contains: + created_by: + description: + - The identity that created the resource. + type: str + returned: always + sample: null + created_by_type: + description: + - The type of identity that created the resource. + returned: always + type: str + sample: null + created_at: + description: + - The timestamp of resource creation (UTC). + returned: always + sample: null + type: str + last_modified_by: + description: + - The identity that last modified the resource. + type: str + returned: always + sample: null + last_modified_by_type: + description: + - The type of identity that last modified the resource. + returned: always + sample: null + type: str + last_modified_at: + description: + - The timestamp of resource last modification (UTC). + returned: always + sample: null + type: str + tags: + description: + - Tags assigned to the resource. Dictionary of string:string pairs. + type: dict + returned: always + sample: { tag1: abc } +''' + + +try: + from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + from azure.core.exceptions import ResourceNotFoundError + from azure.core.polling import LROPoller +except ImportError: + # This is handled in azure_rm_common + pass + + +sku_spec = dict( + name=dict(type='str', required=True), + tier=dict(type='str', required=True, choices=["Burstable", "GeneralPurpose", "MemoryOptimized"]) +) + + +maintenance_window_spec = dict( + custom_window=dict(type='str'), + start_hour=dict(type='int'), + start_minute=dict(type='int'), + day_of_week=dict(type='int'), +) + + +high_availability_spec = dict( + mode=dict(type='str', choices=["Disabled", "ZoneRedundant"]), + standby_availability_zone=dict(type='str') +) + + +network_spec = dict( + delegated_subnet_resource_id=dict(type='str'), + private_dns_zone_arm_resource_id=dict(type='str'), + public_network_access=dict(type='str', choices=["Enabled", "Disabled"]) +) + + +backup_spec = dict( + backup_retention_days=dict(type='int'), + geo_redundant_backup=dict(type='str', choices=["Enabled", "Disabled"]), +) + + +storage_spec = dict( + storage_size_gb=dict(type='int') +) + + +class AzureRMPostgreSqlFlexibleServers(AzureRMModuleBase): + """Configuration class for an Azure RM PostgreSQL Flexible Server resource""" + + def __init__(self): + self.module_arg_spec = dict( + resource_group=dict( + type='str', + required=True + ), + name=dict( + type='str', + required=True + ), + sku=dict( + type='dict', + options=sku_spec + ), + location=dict( + type='str' + ), + administrator_login=dict( + type='str' + ), + administrator_login_password=dict( + type='str', + no_log=True + ), + version=dict( + type='str', + choices=['11', '12', '13'] + ), + fully_qualified_domain_name=dict( + type='str', + ), + storage=dict( + type='dict', + options=storage_spec + ), + backup=dict( + type='dict', + options=backup_spec + ), + network=dict( + type='dict', + options=network_spec + ), + high_availability=dict( + type='dict', + options=high_availability_spec + ), + maintenance_window=dict( + type='dict', + options=maintenance_window_spec + ), + point_in_time_utc=dict( + type='str' + ), + availability_zone=dict( + type='str' + ), + create_mode=dict( + type='str', + choices=['Default', 'Create', 'Update', 'PointInTimeRestore'] + ), + is_start=dict( + type='bool', + default=False, + ), + is_restart=dict( + type='bool', + default=False + ), + is_stop=dict( + type='bool', + default=False + ), + source_server_resource_id=dict( + type='str' + ), + state=dict( + type='str', + default='present', + choices=['present', 'absent'] + ) + ) + + self.resource_group = None + self.name = None + self.parameters = dict() + self.update_parameters = dict() + self.tags = None + self.is_start = None + self.is_stop = None + self.is_restart = None + + self.results = dict(changed=False) + self.state = None + + super(AzureRMPostgreSqlFlexibleServers, self).__init__(derived_arg_spec=self.module_arg_spec, + supports_check_mode=True, + supports_tags=True) + + def exec_module(self, **kwargs): + """Main module execution method""" + + for key in list(self.module_arg_spec.keys()) + ['tags']: + if hasattr(self, key): + setattr(self, key, kwargs[key]) + elif kwargs[key] is not None: + self.parameters[key] = kwargs[key] + for key in ['location', 'sku', 'administrator_login_password', 'storage', 'backup', 'high_availability', 'maintenance_window', 'create_mode']: + self.update_parameters[key] = kwargs[key] + + old_response = None + response = None + changed = False + + resource_group = self.get_resource_group(self.resource_group) + + if "location" not in self.parameters: + self.parameters["location"] = resource_group.location + self.update_parameters["location"] = resource_group.location + + old_response = self.get_postgresqlflexibleserver() + + if not old_response: + self.log("PostgreSQL Flexible Server instance doesn't exist") + if self.state == 'present': + if not self.check_mode: + response = self.create_postgresqlflexibleserver(self.parameters) + if self.is_stop: + self.stop_postgresqlflexibleserver() + elif self.is_start: + self.start_postgresqlflexibleserver() + elif self.is_restart: + self.restart_postgresqlflexibleserver() + changed = True + else: + self.log("PostgreSQL Flexible Server instance doesn't exist, Don't need to delete") + else: + self.log("PostgreSQL Flexible Server instance already exists") + if self.state == 'present': + update_flag = False + if self.update_parameters.get('sku') is not None: + for key in self.update_parameters['sku'].keys(): + if self.update_parameters['sku'][key] is not None and self.update_parameters['sku'][key] != old_response['sku'].get(key): + update_flag = True + else: + self.update_parameters['sku'][key] = old_response['sku'].get(key) + + if self.update_parameters.get('storage') is not None and self.update_parameters['storage'] != old_response['storage']: + update_flag = True + else: + self.update_parameters['storage'] = old_response['storage'] + + if self.update_parameters.get('backup') is not None: + for key in self.update_parameters['backup'].keys(): + if self.update_parameters['backup'][key] is not None and self.update_parameters['backup'][key] != old_response['backup'].get(key): + update_flag = True + else: + self.update_parameters['backup'][key] = old_response['backup'].get(key) + + if self.update_parameters.get('high_availability') is not None: + for key in self.update_parameters['high_availability'].keys(): + if (self.update_parameters['high_availability'][key] is not None) and\ + (self.update_parameters['high_availability'][key] != old_response['high_availability'].get(key)): + update_flag = True + else: + self.update_parameters['high_availability'][key] = old_response['high_availability'].get(key) + + if self.update_parameters.get('maintenance_window') is not None: + for key in self.update_parameters['maintenance_window'].keys(): + if (self.update_parameters['maintenance_window'][key] is not None) and\ + (self.update_parameters['maintenance_window'][key] != old_response['maintenance_window'].get(key)): + update_flag = True + else: + self.update_parameters['maintenance_window'][key] = old_response['maintenance_window'].get(key) + + update_tags, new_tags = self.update_tags(old_response['tags']) + self.update_parameters['tags'] = new_tags + if update_tags: + update_flag = True + + if update_flag: + changed = True + if not self.check_mode: + response = self.update_postgresqlflexibleserver(self.update_parameters) + else: + response = old_response + if self.is_stop: + self.stop_postgresqlflexibleserver() + changed = True + elif self.is_start: + self.start_postgresqlflexibleserver() + changed = True + elif self.is_restart: + self.restart_postgresqlflexibleserver() + changed = True + else: + if not self.check_mode: + if self.is_stop: + self.stop_postgresqlflexibleserver() + changed = True + elif self.is_start: + self.start_postgresqlflexibleserver() + changed = True + elif self.is_restart: + self.restart_postgresqlflexibleserver() + changed = True + response = old_response + else: + self.log("PostgreSQL Flexible Server instance already exist, will be deleted") + changed = True + if not self.check_mode: + response = self.delete_postgresqlflexibleserver() + + self.results['changed'] = changed + self.results['state'] = response + + return self.results + + def update_postgresqlflexibleserver(self, body): + ''' + Updates PostgreSQL Flexible Server with the specified configuration. + :return: deserialized PostgreSQL Flexible Server instance state dictionary + ''' + self.log("Updating the PostgreSQL Flexible Server instance {0}".format(self.name)) + try: + # structure of parameters for update must be changed + response = self.postgresql_flexible_client.servers.begin_update(resource_group_name=self.resource_group, + server_name=self.name, + parameters=body) + if isinstance(response, LROPoller): + response = self.get_poller_result(response) + + except Exception as exc: + self.log('Error attempting to create the PostgreSQL Flexible Server instance.') + self.fail("Error updating the PostgreSQL Flexible Server instance: {0}".format(str(exc))) + return self.format_item(response) + + def create_postgresqlflexibleserver(self, body): + ''' + Creates PostgreSQL Flexible Server with the specified configuration. + :return: deserialized PostgreSQL Flexible Server instance state dictionary + ''' + self.log("Creating the PostgreSQL Flexible Server instance {0}".format(self.name)) + try: + response = self.postgresql_flexible_client.servers.begin_create(resource_group_name=self.resource_group, + server_name=self.name, + parameters=body) + if isinstance(response, LROPoller): + response = self.get_poller_result(response) + + except Exception as exc: + self.log('Error attempting to create the PostgreSQL Flexible Server instance.') + self.fail("Error creating the PostgreSQL Flexible Server instance: {0}".format(str(exc))) + return self.format_item(response) + + def delete_postgresqlflexibleserver(self): + ''' + Deletes specified PostgreSQL Flexible Server instance in the specified subscription and resource group. + + :return: True + ''' + self.log("Deleting the PostgreSQL Flexible Server instance {0}".format(self.name)) + try: + self.postgresql_flexible_client.servers.begin_delete(resource_group_name=self.resource_group, + server_name=self.name) + except Exception as e: + self.log('Error attempting to delete the PostgreSQL Flexible Server instance.') + self.fail("Error deleting the PostgreSQL Flexible Server instance: {0}".format(str(e))) + + def stop_postgresqlflexibleserver(self): + ''' + Stop PostgreSQL Flexible Server instance in the specified subscription and resource group. + + :return: True + ''' + self.log("Stop the PostgreSQL Flexible Server instance {0}".format(self.name)) + try: + self.postgresql_flexible_client.servers.begin_stop(resource_group_name=self.resource_group, + server_name=self.name) + except Exception as e: + self.log('Error attempting to stop the PostgreSQL Flexible Server instance.') + self.fail("Error stop the PostgreSQL Flexible Server instance: {0}".format(str(e))) + + def start_postgresqlflexibleserver(self): + ''' + Start PostgreSQL Flexible Server instance in the specified subscription and resource group. + + :return: True + ''' + self.log("Starting the PostgreSQL Flexible Server instance {0}".format(self.name)) + try: + self.postgresql_flexible_client.servers.begin_start(resource_group_name=self.resource_group, + server_name=self.name) + except Exception as e: + self.log('Error attempting to start the PostgreSQL Flexible Server instance.') + self.fail("Error starting the PostgreSQL Flexible Server instance: {0}".format(str(e))) + + def restart_postgresqlflexibleserver(self): + ''' + Restart PostgreSQL Flexible Server instance in the specified subscription and resource group. + + :return: True + ''' + self.log("Restarting the PostgreSQL Flexible Server instance {0}".format(self.name)) + try: + self.postgresql_flexible_client.servers.begin_restart(resource_group_name=self.resource_group, + server_name=self.name) + except Exception as e: + self.log('Error attempting to restart the PostgreSQL Flexible Server instance.') + self.fail("Error restarting the PostgreSQL Flexible Server instance: {0}".format(str(e))) + + def get_postgresqlflexibleserver(self): + ''' + Gets the properties of the specified PostgreSQL Flexible Server. + + :return: deserialized PostgreSQL Flexible Server instance state dictionary + ''' + self.log("Checking if the PostgreSQL Flexible Server instance {0} is present".format(self.name)) + try: + response = self.postgresql_flexible_client.servers.get(resource_group_name=self.resource_group, + server_name=self.name) + self.log("Response : {0}".format(response)) + self.log("PostgreSQL Flexible Server instance : {0} found".format(response.name)) + except ResourceNotFoundError as e: + self.log('Did not find the PostgreSQL Flexible Server instance. Exception as {0}'.format(str(e))) + return None + + return self.format_item(response) + + def format_item(self, item): + result = dict( + id=item.id, + resource_group=self.resource_group, + name=item.name, + sku=dict(), + location=item.location, + tags=item.tags, + system_data=dict(), + administrator_login=item.administrator_login, + version=item.version, + minor_version=item.minor_version, + fully_qualified_domain_name=item.fully_qualified_domain_name, + storage=dict(), + backup=dict(), + network=dict(), + high_availability=dict(), + maintenance_window=dict(), + source_server_resource_id=item.source_server_resource_id, + point_in_time_utc=item.point_in_time_utc, + availability_zone=item.availability_zone, + ) + if item.sku is not None: + result['sku']['name'] = item.sku.name + result['sku']['tier'] = item.sku.tier + if item.system_data is not None: + result['system_data']['created_by'] = item.system_data.created_by + result['system_data']['created_by_type'] = item.system_data.created_by_type + result['system_data']['created_at'] = item.system_data.created_at + result['system_data']['last_modified_by'] = item.system_data.last_modified_by + result['system_data']['last_modified_by_type'] = item.system_data.last_modified_by_type + result['system_data']['last_modified_at'] = item.system_data.last_modified_at + if item.storage is not None: + result['storage']['storage_size_gb'] = item.storage.storage_size_gb + if item.backup is not None: + result['backup']['backup_retention_days'] = item.backup.backup_retention_days + result['backup']['geo_redundant_backup'] = item.backup.geo_redundant_backup + if item.network is not None: + result['network']['public_network_access'] = item.network.public_network_access + result['network']['delegated_subnet_resource_id'] = item.network.delegated_subnet_resource_id + result['network']['private_dns_zone_arm_resource_id'] = item.network.private_dns_zone_arm_resource_id + if item.high_availability is not None: + result['high_availability']['mode'] = item.high_availability.mode + result['high_availability']['standby_availability_zone'] = item.high_availability.standby_availability_zone + if item.maintenance_window is not None: + result['maintenance_window']['custom_window'] = item.maintenance_window.custom_window + result['maintenance_window']['start_minute'] = item.maintenance_window.start_minute + result['maintenance_window']['start_hour'] = item.maintenance_window.start_hour + result['maintenance_window']['day_of_week'] = item.maintenance_window.day_of_week + + return result + + +def main(): + """Main execution""" + AzureRMPostgreSqlFlexibleServers() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibleserver_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibleserver_info.py new file mode 100644 index 000000000..50fe9adc5 --- /dev/null +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_postgresqlflexibleserver_info.py @@ -0,0 +1,443 @@ +#!/usr/bin/python +# +# Copyright (c) 2024 xuzhang3 (@xuzhang3), Fred-sun (@Fred-sun) +# +# 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 + + +DOCUMENTATION = ''' +--- +module: azure_rm_postgresqlflexibleserver_info +version_added: "2.2.0" +short_description: Get Azure PostgreSQL Flexible Server facts +description: + - Get facts of PostgreSQL Flexible Server. + +options: + resource_group: + description: + - The name of the resource group that contains the resource. You can obtain this value from the Azure Resource Manager API or the portal. + type: str + name: + description: + - The name of the server. + type: str + tags: + description: + - Limit results by providing a list of tags. Format tags as 'key' or 'key:value'. + type: list + elements: str + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - xuzhang3 (@xuzhang3) + - Fred-sun (@Fred-sun) + +''' + +EXAMPLES = ''' +- name: Get instance of PostgreSQL Flexible Server + azure_rm_postgresqlflexibleserver_info: + resource_group: myResourceGroup + name: server_name + +- name: List instances of PostgreSQL Flexible Server + azure_rm_postgresqlflexibleserver_info: + resource_group: myResourceGroup + tags: + - key +''' + +RETURN = ''' +servers: + description: + - A list of dictionaries containing facts for PostgreSQL Flexible servers. + returned: always + type: complex + contains: + id: + description: + - Resource ID of the postgresql flexible server. + returned: always + type: str + sample: "/subscriptions/xxx/resourceGroups/myResourceGroup/providers/Microsoft.DBforPostgreSQL/flexibleservers/postgresql3" + resource_group: + description: + - Resource group name. + returned: always + type: str + sample: myResourceGroup + name: + description: + - Resource name. + returned: always + type: str + sample: postgreabdud1223 + location: + description: + - The location the resource resides in. + returned: always + type: str + sample: eastus + sku: + description: + - The SKU of the server. + returned: always + type: complex + contains: + name: + description: + - The name of the SKU. + returned: always + type: str + sample: Standard_B1ms + tier: + description: + - The tier of the particular SKU. + returned: always + type: str + sample: Burstable + storage: + description: + - The maximum storage allowed for a server. + returned: always + type: complex + contains: + storage_size_gb: + description: + - Max storage allowed for a server. + type: int + returned: always + sample: 128 + administrator_login: + description: + - The administrator's login name of a server. + returned: always + type: str + sample: azureuser + version: + description: + - Flexible Server version. + returned: always + type: str + sample: "12" + fully_qualified_domain_name: + description: + - The fully qualified domain name of the flexible server. + returned: always + type: str + sample: postflexiblefredpgsqlflexible.postgres.database.azure.com + availability_zone: + description: + - availability zone information of the server. + type: str + returned: always + sample: 1 + backup: + description: + - Backup properties of a server. + type: complex + returned: always + contains: + backup_retention_days: + description: + - Backup retention days for the server. + type: int + returned: always + sample: 7 + geo_redundant_backup: + description: + - A value indicating whether Geo-Redundant backup is enabled on the server. + type: str + returned: always + sample: Disabled + high_availability: + description: + - High availability properties of a server. + type: complex + returned: always + contains: + mode: + description: + - The HA mode for the server. + returned: always + sample: Disabled + type: str + standby_availability_zone: + description: + - availability zone information of the standby. + type: str + returned: always + sample: null + maintenance_window: + description: + - Maintenance window properties of a server. + type: complex + returned: always + contains: + custom_window: + description: + - Indicates whether custom window is enabled or disabled. + returned: always + sample: Enabled + type: str + day_of_week: + description: + - Day of week for maintenance window. + returned: always + sample: 0 + type: int + start_hour: + description: + - Start hour for maintenance window. + type: int + returned: always + sample: 8 + start_minute: + description: + - Start minute for maintenance window. + type: int + returned: always + sample: 0 + network: + description: + - Network properties of a server. + type: complex + returned: always + contains: + delegated_subnet_resource_id: + description: + - Delegated subnet arm resource id. + type: str + returned: always + sample: null + private_dns_zone_arm_resource_id: + description: + - Private dns zone arm resource id. + type: str + returned: always + sample: null + public_network_access: + description: + - Public network access is enabled or not. + type: str + returned: always + sample: Enabled + point_in_time_utc: + description: + - Restore point creation time (ISO8601 format). + type: str + sample: null + returned: always + source_server_resource_id: + description: + - The source server resource ID to restore from. + type: str + returned: always + sample: null + system_data: + description: + - The system metadata relating to this resource. + type: complex + returned: always + contains: + created_by: + description: + - The identity that created the resource. + type: str + returned: always + sample: null + created_by_type: + description: + - The type of identity that created the resource. + returned: always + type: str + sample: null + created_at: + description: + - The timestamp of resource creation (UTC). + returned: always + sample: null + type: str + last_modified_by: + description: + - The identity that last modified the resource. + type: str + returned: always + sample: null + last_modified_by_type: + description: + - The type of identity that last modified the resource. + returned: always + sample: null + type: str + last_modified_at: + description: + - The timestamp of resource last modification (UTC). + returned: always + sample: null + type: str + tags: + description: + - Tags assigned to the resource. Dictionary of string:string pairs. + type: dict + returned: always + sample: { tag1: abc } +''' + + +try: + from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + from azure.core.exceptions import ResourceNotFoundError +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMPostgreSqlFlexibleServersInfo(AzureRMModuleBase): + def __init__(self): + # define user inputs into argument + self.module_arg_spec = dict( + resource_group=dict( + type='str', + ), + name=dict( + type='str' + ), + tags=dict( + type='list', + elements='str' + ) + ) + # store the results of the module operation + self.results = dict( + changed=False + ) + self.resource_group = None + self.name = None + self.tags = None + super(AzureRMPostgreSqlFlexibleServersInfo, self).__init__(self.module_arg_spec, supports_check_mode=True, 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.resource_group is not None and self.name is not None: + self.results['servers'] = self.get() + elif self.resource_group is not None: + self.results['servers'] = self.list_by_resource_group() + else: + self.results['servers'] = self.list_all() + return self.results + + def get(self): + response = None + results = [] + try: + response = self.postgresql_flexible_client.servers.get(resource_group_name=self.resource_group, + server_name=self.name) + self.log("Response : {0}".format(response)) + except ResourceNotFoundError: + self.log('Could not get facts for PostgreSQL Flexible Server.') + + if response and self.has_tags(response.tags, self.tags): + results.append(self.format_item(response)) + + return results + + def list_by_resource_group(self): + response = None + results = [] + try: + response = self.postgresql_flexible_client.servers.list_by_resource_group(resource_group_name=self.resource_group) + self.log("Response : {0}".format(response)) + except Exception: + self.log('Could not get facts for PostgreSQL Flexible Servers.') + + if response is not None: + for item in response: + if self.has_tags(item.tags, self.tags): + results.append(self.format_item(item)) + + return results + + def list_all(self): + response = None + results = [] + try: + response = self.postgresql_flexible_client.servers.list() + self.log("Response : {0}".format(response)) + except Exception: + self.log('Could not get facts for PostgreSQL Flexible Servers.') + + if response is not None: + for item in response: + if self.has_tags(item.tags, self.tags): + results.append(self.format_item(item)) + + return results + + def format_item(self, item): + result = dict( + id=item.id, + resource_group=self.resource_group, + name=item.name, + sku=dict(), + location=item.location, + tags=item.tags, + system_data=dict(), + administrator_login=item.administrator_login, + version=item.version, + minor_version=item.minor_version, + fully_qualified_domain_name=item.fully_qualified_domain_name, + storage=dict(), + backup=dict(), + network=dict(), + high_availability=dict(), + maintenance_window=dict(), + source_server_resource_id=item.source_server_resource_id, + point_in_time_utc=item.point_in_time_utc, + availability_zone=item.availability_zone, + ) + if item.sku is not None: + result['sku']['name'] = item.sku.name + result['sku']['tier'] = item.sku.tier + if item.system_data is not None: + result['system_data']['created_by'] = item.system_data.created_by + result['system_data']['created_by_type'] = item.system_data.created_by_type + result['system_data']['created_at'] = item.system_data.created_at + result['system_data']['last_modified_by'] = item.system_data.last_modified_by + result['system_data']['last_modified_by_type'] = item.system_data.last_modified_by_type + result['system_data']['last_modified_at'] = item.system_data.last_modified_at + if item.storage is not None: + result['storage']['storage_size_gb'] = item.storage.storage_size_gb + if item.backup is not None: + result['backup']['backup_retention_days'] = item.backup.backup_retention_days + result['backup']['geo_redundant_backup'] = item.backup.geo_redundant_backup + if item.network is not None: + result['network']['public_network_access'] = item.network.public_network_access + result['network']['delegated_subnet_resource_id'] = item.network.delegated_subnet_resource_id + result['network']['private_dns_zone_arm_resource_id'] = item.network.private_dns_zone_arm_resource_id + if item.high_availability is not None: + result['high_availability']['mode'] = item.high_availability.mode + result['high_availability']['standby_availability_zone'] = item.high_availability.standby_availability_zone + if item.maintenance_window is not None: + result['maintenance_window']['custom_window'] = item.maintenance_window.custom_window + result['maintenance_window']['start_minute'] = item.maintenance_window.start_minute + result['maintenance_window']['start_hour'] = item.maintenance_window.start_hour + result['maintenance_window']['day_of_week'] = item.maintenance_window.day_of_week + + return result + + +def main(): + AzureRMPostgreSqlFlexibleServersInfo() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_privatednsrecordset.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_privatednsrecordset.py index 3560139e7..f2e9e92f7 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_privatednsrecordset.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_privatednsrecordset.py @@ -324,8 +324,10 @@ class AzureRMPrivateDNSRecordSet(AzureRMModuleBase): resource_group=dict(type='str', required=True), relative_name=dict(type='str', required=True), zone_name=dict(type='str', required=True), - record_type=dict(choices=RECORD_ARGSPECS.keys(), required=True, type='str'), - record_mode=dict(choices=['append', 'purge'], default='purge'), + record_type=dict(choices=['A', 'AAAA', 'CNAME', 'MX', 'PTR', 'SRV', 'TXT', 'SOA'], + required=True, + type='str'), + record_mode=dict(type='str', choices=['append', 'purge'], default='purge'), state=dict(choices=['present', 'absent'], default='present', type='str'), time_to_live=dict(type='int', default=3600), records=dict(type='list', elements='dict') diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_publicipprefix.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_publicipprefix.py new file mode 100644 index 000000000..90bfafc41 --- /dev/null +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_publicipprefix.py @@ -0,0 +1,455 @@ +#!/usr/bin/python +# +# Copyright (c) 2024 xuzhang3 (@xuzhang3), Fred-Sun (@Fred-Sun) +# +# 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 + + +DOCUMENTATION = ''' +--- +module: azure_rm_publicipprefix + +version_added: "2.2.0" + +short_description: Manage Azure Public IP prefix + +description: + - Create, update and delete a Public IP prefix. + +options: + resource_group: + description: + - Name of resource group with which the Public IP prefix is associated. + required: true + type: str + name: + description: + - Name of the Public IP prefix. + required: true + type: str + state: + description: + - Assert the state of the Public IP. Use C(present) to create or update a and C(absent) to delete. + default: present + type: str + choices: + - absent + - present + location: + description: + - Valid Azure location. Defaults to location of the resource group. + type: str + sku: + description: + - The public IP prefix SKU. + type: dict + suboptions: + name: + description: + - Name of a public IP prefix SKU. + type: str + choices: + - Standard + tier: + description: + - Tier of a public IP prefix SKU. + type: str + choices: + - Regional + - Global + custom_ip_prefix: + description: + - The Custom IP prefix that this prefix is associated with. + type: dict + suboptions: + id: + description: + - Resource ID. + type: str + extended_location: + description: + - The extended location of the public ip address. + type: str + ip_tags: + description: + - The list of tags associated with the public IP prefix. + type: list + elements: dict + suboptions: + ip_tag_type: + description: + - The IP tag type. Example as FirstPartyUsage. + type: str + tag: + description: + - The value of the IP tag associated with the public IP. Example as SQL. + type: str + public_ip_address_version: + description: + - The public IP address version. + type: str + choices: + - IPV4 + - IPV6 + zones: + description: + - A list of availability zones denoting the IP prefix allocated for the resource needs to come from. + type: list + elements: str + choices: + - '1' + - '2' + - '3' + prefix_length: + description: + - The Length of the Public IP Prefix. + type: int + +extends_documentation_fragment: + - azure.azcollection.azure + - azure.azcollection.azure_tags + +author: + - xuzhang3 (@xuzhang3) + - Fred-sun (@Fred-sun) +''' + +EXAMPLES = ''' +- name: Create a public ip prefix + azure_rm_publicipprefix: + resource_group: myResourceGroup + name: my_public_ip + public_ip_address_version: IPV4 + prefix_length: 29 + sku: + name: Standard + tier: Regional + zones: + - 1 + tags: + key1: value1 + +- name: Delete public ip prefix + azure_rm_publicipprefix: + resource_group: myResourceGroup + name: my_public_ipprefix + state: absent +''' + +RETURN = ''' +state: + description: + - List of public IP prefixes dicts. + returned: always + type: complex + contains: + id: + description: + - Resource ID. + returned: always + type: str + sample: /subscriptions/xxx---xxxxx/resourceGroups/v-xisuRG/providers/Microsoft.Network/publicIPPrefixes/pipb57dc95224 + name: + description: + - Name of the public IP prefix. + returned: always + type: str + sample: prefix57dc95224 + type: + description: + - Resource type. + returned: always + type: str + sample: "Microsoft.Network/publicIPPrefixes" + location: + description: + - Resource location. + returned: always + type: str + sample: eastus + tags: + description: + - Resource tags. + returned: always + type: dict + sample: { + "delete": "on-exit", + "testing": "testing" + } + public_ip_address_version: + description: + - The public IP address version. + - Possible values are C(IPv4) and C(IPv6). + returned: always + type: str + sample: IPv4 + ip_tags: + description: + - The list of tags associated with the public IP prefixes. + returned: always + type: list + sample: [{'type': 'FirstPartyUsage', 'value': 'Storage'}] + resource_guid: + description: + - The resource GUID property of the public IP prefix resource. + type: str + sample: "47cafa04-851d-4579-894d-74ad6afe3233" + custom_ip_prefix: + description: + - The customIpPrefix that this prefix is associated with. + type: dict + returned: always + sample: {} + public_ip_addresses: + description: + - The list of all referenced PublicIPAddresses. + type: list + sample: [] +''' + +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + +try: + from azure.core.exceptions import ResourceNotFoundError +except ImportError: + # This is handled in azure_rm_common + pass + + +def prefix_to_dict(prefix): + result = dict( + id=prefix.id, + name=prefix.name, + tags=prefix.tags, + type=prefix.type, + location=prefix.location, + public_ip_address_version=prefix.public_ip_address_version, + prefix_length=prefix.prefix_length, + provisioning_state=prefix.provisioning_state, + etag=prefix.etag, + zones=prefix.zones, + sku=dict(), + ip_tags=list(), + custom_ip_prefix=dict(), + ip_prefix=prefix.ip_prefix, + resource_guid=prefix.resource_guid, + public_ip_addresses=list(), + extended_location=prefix.extended_location + ) + if prefix.public_ip_addresses: + result['public_ip_addresses'] = [x.id for x in prefix.public_ip_addresses] + if prefix.sku: + result['sku']['name'] = prefix.sku.name + result['sku']['tier'] = prefix.sku.tier + if prefix.custom_ip_prefix: + result['custom_ip_prefix']['id'] = prefix.custom_ip_prefix.id + if prefix.ip_tags: + result['ip_tags'] = [dict(type=x.ip_tag_type, value=x.tag) for x in prefix.ip_tags] + return result + + +class AzureRMPublicIPPrefix(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + resource_group=dict(type='str', required=True), + name=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', 'absent']), + location=dict(type='str'), + public_ip_address_version=dict(type='str', choices=['IPV4', 'IPV6']), + extended_location=dict(type='str'), + prefix_length=dict(type='int'), + custom_ip_prefix=dict( + type='dict', + options=dict( + id=dict(type='str') + ) + ), + sku=dict( + type='dict', + options=dict( + name=dict(type='str', choices=['Standard']), + tier=dict(type='str', choices=['Regional', 'Global']) + ) + ), + ip_tags=dict( + type='list', + elements='dict', + options=dict( + ip_tag_type=dict(type='str'), + tag=dict(type='str') + ) + ), + zones=dict(type='list', elements='str', choices=['1', '2', '3']) + ) + + self.resource_group = None + self.name = None + self.location = None + self.state = None + self.tags = None + self.zones = None + self.sku = None + self.ip_tags = None + self.public_ip_address_version = None + self.prefix_length = None + self.custom_ip_prefix = None + + self.results = dict( + changed=False, + state=dict() + ) + + super(AzureRMPublicIPPrefix, self).__init__(derived_arg_spec=self.module_arg_spec, + supports_check_mode=True) + + def exec_module(self, **kwargs): + + for key in list(self.module_arg_spec.keys()) + ['tags']: + setattr(self, key, kwargs[key]) + + results = dict() + changed = False + prefix = None + update_tags = False + + resource_group = self.get_resource_group(self.resource_group) + if not self.location: + # Set default location + self.location = resource_group.location + + try: + self.log("Fetch public ip prefix {0}".format(self.name)) + prefix = self.network_client.public_ip_prefixes.get(self.resource_group, self.name) + self.log("Public IP prefix {0} exists".format(self.name)) + + if self.state == 'present': + results = prefix_to_dict(prefix) + + if self.public_ip_address_version is not None and \ + self.public_ip_address_version.lower() != results['public_ip_address_version'].lower(): + changed = False + results['public_ip_address_version'] = self.public_ip_address_version + self.fail("The public_ip_address_version can't be updated") + + if self.prefix_length is not None and self.prefix_length != results['prefix_length']: + changed = False + results['prefix_length'] = self.prefix_length + self.fail("The prefix_length can't be updated") + + if self.sku is not None: + for key in self.sku.keys(): + if self.sku[key] != results['sku'].get(key): + changed = False + self.fail("The sku can't be updated") + results['sku'] = self.sku + + if self.zones is not None and not all(key in results['zones'] for key in self.zones): + changed = False + results['zones'] = self.zones + self.fail("The zone can't be updated") + + if self.extended_location is not None and self.extended_location != results['extended_location']: + changed = False + results['extended_location'] = self.extended_location + self.fail("The extended_location can't be updated") + + if self.ip_tags is not None: + for key in self.ip_tags.keys(): + if self.ip_tags[key] != results['ip_tags'].get(key): + changed = False + results['ip_tags'] = self.ip_tags + self.fail("The ip_tags can't be updated") + + if self.custom_ip_prefix is not None: + if results.get('custom_ip_prefix') is None: + changed = False + results['custom_ip_prefix'] = self.custom_ip_prefix + self.fail("The custom_ip_prefix can't be updated") + elif self.custom_ip_prefix['id'].lower() != results['custom_ip_prefix']['id'].lower(): + changed = False + results['custom_ip_prefix'] = self.custom_ip_prefix + self.fail("The custom_ip_prefix can't be updated") + + update_tags, results['tags'] = self.update_tags(results['tags']) + if update_tags: + self.log("CHANGED: tags") + changed = True + self.tags = results['tags'] + + elif self.state == 'absent': + self.log("CHANGED: public ip prefix {0} exists but requested state is 'absent'".format(self.name)) + changed = True + except ResourceNotFoundError: + self.log('Public ip prefix {0} does not exist'.format(self.name)) + if self.state == 'present': + self.log("CHANGED: public IP prefix {0} does not exist but requested state is 'present'".format(self.name)) + changed = True + + self.results['state'] = results + self.results['changed'] = changed + + if self.check_mode: + results['changed'] = True + return results + + if update_tags: + self.results['state'] = self.update_prefix_tags(self.tags) + + elif changed: + if self.state == 'present': + self.log("Create or Update Public IP prefix {0}".format(self.name)) + prefix = self.network_models.PublicIPPrefix( + location=self.location, + public_ip_address_version=self.public_ip_address_version, + prefix_length=self.prefix_length, + zones=self.zones, + tags=self.tags, + sku=self.sku, + ip_tags=self.ip_tags, + custom_ip_prefix=self.custom_ip_prefix + ) + self.results['state'] = self.create_or_update_prefix(prefix) + + elif self.state == 'absent': + self.log('Delete public ip {0}'.format(self.name)) + self.delete_prefix() + + return self.results + + def update_prefix_tags(self, tags): + try: + prefix = self.network_client.public_ip_prefixes.update_tags(self.resource_group, self.name, dict(tags=tags)) + except Exception as exc: + self.fail("Error updating tags {0} - {1}".format(self.name, str(exc))) + return prefix_to_dict(prefix) + + def create_or_update_prefix(self, prefix): + try: + poller = self.network_client.public_ip_prefixes.begin_create_or_update(self.resource_group, self.name, prefix) + prefix = self.get_poller_result(poller) + except Exception as exc: + self.fail("Error creating or updating {0} - {1}".format(self.name, str(exc))) + return prefix_to_dict(prefix) + + def delete_prefix(self): + try: + poller = self.network_client.public_ip_prefixes.begin_delete(self.resource_group, self.name) + self.get_poller_result(poller) + except Exception as exc: + self.fail("Error deleting {0} - {1}".format(self.name, str(exc))) + + self.results['state']['status'] = 'Deleted' + return True + + +def main(): + AzureRMPublicIPPrefix() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_publicipprefix_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_publicipprefix_info.py new file mode 100644 index 000000000..c8cf0eff1 --- /dev/null +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_publicipprefix_info.py @@ -0,0 +1,296 @@ +#!/usr/bin/python +# +# Copyright (c) 2024 xuzhang3 (@xuzhang3), Fred-Sun (@Fred-Sun) +# +# 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 + + +DOCUMENTATION = ''' +--- +module: azure_rm_publicipprefix_info + +version_added: "2.2.0" + +short_description: Get public IP prefix facts + +description: + - Get facts for a specific public IP prefix. + - Get all facts for a specific public IP prefixes within a resource group. + +options: + name: + description: + - The name of the public IP prefix. + type: str + resource_group: + description: + - Limit results by resource group. Required when using name parameter. + type: str + tags: + description: + - Limit results by providing a list of tags. Format tags as 'key' or 'key:value'. + type: list + elements: str + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - xuzhang3 (@xuzhang3) + - Fred-sun (@Fred-sun) +''' + +EXAMPLES = ''' +- name: Get facts for one Public IP Prefix + azure_rm_publicipprefix_info: + resource_group: myResourceGroup + name: publicipprefix + +- name: Get facts for all Public IPs within a resource groups + azure_rm_publicipprefix_info: + resource_group: myResourceGroup + tags: + - key:value +''' + +RETURN = ''' +publicipprefix: + description: + - List of public IP prefixes dicts. + returned: always + type: complex + contains: + id: + description: + - Resource ID. + returned: always + type: str + sample: /subscriptions/xxx---xxxxx/resourceGroups/v-xisuRG/providers/Microsoft.Network/publicIPPrefixes/pipb57dc95224 + name: + description: + - Name of the public IP prefix. + returned: always + type: str + sample: prefix57dc95224 + type: + description: + - Resource type. + returned: always + type: str + sample: "Microsoft.Network/publicIPPrefixes" + location: + description: + - Resource location. + returned: always + type: str + sample: eastus + tags: + description: + - Resource tags. + returned: always + type: dict + sample: { + "delete": "on-exit", + "testing": "testing" + } + public_ip_address_version: + description: + - The public IP address version. + - Possible values are C(IPv4) and C(IPv6). + returned: always + type: str + sample: IPv4 + ip_tags: + description: + - The list of tags associated with the public IP prefixes. + returned: always + type: list + sample: [ + { + "type": "FirstPartyUsage", + "value": "Storage" + } + ] + provisioning_state: + description: + - The provisioning state of the PublicIP Prefix resource. + - Possible values is C(Succeeded). + returned: always + type: str + sample: Succeeded + etag: + description: + - A unique read-only string that changes whenever the resource is updated. + returned: always + type: str + sample: "W/'1905ee13-7623-45b1-bc6b-4a12b2fb9d15'" + sku: + description: + - The public IP prefix SKU. + returned: always + type: dict + sample: {'name': 'standard', 'tier': 'Regional'} + zones: + description: + - A list of availability zones denoting the IP allocated for the resource needs to come from. + returned: always + type: list + sample: ['1', '2'] + prefix_length: + description: + - The Length of the Public IP Prefix. + type: int + returned: always + sample: 29 + extended_location: + description: + - The extended location of the public ip address. + type: str + returned: always + sample: 'eastus2' + custom_ip_prefix: + description: + - The customIpPrefix that this prefix is associated with. + type: dict + returned: always + sample: {} + public_ip_addresses: + description: + - The list of all referenced PublicIPAddresses. + type: list + sample: [] + resource_guid: + description: + - The resource GUID property of the public IP prefix resource. + type: str + sample: "47cafa04-851d-4579-894d-74ad6afe3233" + ip_prefix: + description: + - The allocated Prefix. + type: str + sample: 20.199.95.80/29 +''' +try: + from azure.core.exceptions import ResourceNotFoundError +except Exception: + # This is handled in azure_rm_common + pass + +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + +AZURE_OBJECT_CLASS = 'PublicIpPrefix' + + +class AzureRMPublicIPPrefixInfo(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + name=dict(type='str'), + resource_group=dict(type='str'), + tags=dict(type='list', elements='str') + ) + + self.results = dict( + changed=False, + ) + + self.name = None + self.resource_group = None + self.tags = None + + super(AzureRMPublicIPPrefixInfo, self).__init__(self.module_arg_spec, + supports_check_mode=True, + supports_tags=False, + facts_module=True) + + def exec_module(self, **kwargs): + for key in self.module_arg_spec: + setattr(self, key, kwargs[key]) + + result = [] + if self.name is not None and self.resource_group is not None: + result = self.get_item() + elif self.resource_group: + result = self.list_resource_group() + else: + result = self.list_all() + + raw = self.filter(result) + + self.results['publicipprefixes'] = self.format(raw) + + return self.results + + def format(self, raw): + return [self.prefix_to_dict(item) for item in raw] + + def filter(self, response): + return [item for item in response if self.has_tags(item.tags, self.tags)] + + def prefix_to_dict(self, prefix): + result = dict( + id=prefix.id, + name=prefix.name, + tags=prefix.tags, + type=prefix.type, + location=prefix.location, + public_ip_address_version=prefix.public_ip_address_version, + prefix_length=prefix.prefix_length, + provisioning_state=prefix.provisioning_state, + etag=prefix.etag, + zones=prefix.zones, + sku=dict(), + ip_tags=list(), + custom_ip_prefix=dict(), + ip_prefix=prefix.ip_prefix, + resource_guid=prefix.resource_guid, + public_ip_addresses=list(), + extended_location=prefix.extended_location + ) + if prefix.public_ip_addresses: + result['public_ip_addresses'] = [x.id for x in prefix.public_ip_addresses] + if prefix.sku: + result['sku']['name'] = prefix.sku.name + result['sku']['tier'] = prefix.sku.tier + if prefix.custom_ip_prefix: + result['custom_ip_prefix']['id'] = prefix.custom_ip_prefix.id + if prefix.ip_tags: + result['ip_tags'] = [dict(type=x.ip_tag_type, value=x.tag) for x in prefix.ip_tags] + return result + + def get_item(self): + self.log('Get properties for {0}'.format(self.name)) + item = None + try: + item = self.network_client.public_ip_prefixes.get(self.resource_group, self.name) + except ResourceNotFoundError: + pass + return [item] if item else [] + + def list_resource_group(self): + self.log('List items in resource groups') + try: + response = self.network_client.public_ip_prefixes.list(self.resource_group) + except ResourceNotFoundError as exc: + self.fail("Error listing items in resource groups {0} - {1}".format(self.resource_group, str(exc))) + return response + + def list_all(self): + self.log('List all items') + try: + response = self.network_client.public_ip_prefixes.list_all() + except ResourceNotFoundError as exc: + self.fail("Error listing all items - {0}".format(str(exc))) + return response + + +def main(): + AzureRMPublicIPPrefixInfo() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_recoveryservicesvault.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_recoveryservicesvault.py index b2c45b2d5..818d846bf 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_recoveryservicesvault.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_recoveryservicesvault.py @@ -208,7 +208,6 @@ class AzureRMRecoveryServicesVault(AzureRMModuleBaseExt): response = None self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) old_response = self.get_resource() @@ -247,10 +246,12 @@ class AzureRMRecoveryServicesVault(AzureRMModuleBaseExt): self.log('Error in creating Azure Recovery Service Vault.') self.fail('Error in creating Azure Recovery Service Vault {0}'.format(str(e))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_recoveryservicesvault_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_recoveryservicesvault_info.py index 6b2dee776..bc26db2dd 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_recoveryservicesvault_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_recoveryservicesvault_info.py @@ -12,9 +12,9 @@ DOCUMENTATION = \ --- module: azure_rm_recoveryservicesvault_info version_added: '1.1.0' -short_description: Get Azure Recovery Services vault Details +short_description: Get or list the Azure Recovery Services vault Details description: - - Get Azure Recovery Services vault Details. + - Get or list the Azure Recovery Services vault Details. options: resource_group: description: @@ -23,8 +23,8 @@ options: type: str name: description: + - If this parameter is not configured, all Vaults in the resource group are listed. - The name of the Azure Recovery Service Vault. - required: true type: str extends_documentation_fragment: - azure.azcollection.azure @@ -34,6 +34,10 @@ author: ''' EXAMPLES = ''' +- name: List all Azure Recovery Services Vault in same resource group + azure_rm_recoveryservicesvault_info: + resource_group: 'myResourceGroup' + - name: Get Azure Recovery Services Vault Details. azure_rm_recoveryservicesvault_info: resource_group: 'myResourceGroup' @@ -110,7 +114,6 @@ class AzureRMRecoveryServicesVaultInfo(AzureRMModuleBaseExt): ), name=dict( type='str', - required=True ) ) @@ -137,13 +140,21 @@ class AzureRMRecoveryServicesVaultInfo(AzureRMModuleBaseExt): return '2016-06-01' def get_url(self): - return '/subscriptions/' \ - + self.subscription_id \ - + '/resourceGroups/' \ - + self.resource_group \ - + '/providers/Microsoft.RecoveryServices' \ - + '/vaults' + '/' \ - + self.name + if self.name is not None: + return '/subscriptions/' \ + + self.subscription_id \ + + '/resourceGroups/' \ + + self.resource_group \ + + '/providers/Microsoft.RecoveryServices' \ + + '/vaults' + '/' \ + + self.name + else: + return '/subscriptions/' \ + + self.subscription_id \ + + '/resourceGroups/' \ + + self.resource_group \ + + '/providers/Microsoft.RecoveryServices' \ + + '/vaults' def exec_module(self, **kwargs): for key in list(self.module_arg_spec.keys()): @@ -160,10 +171,9 @@ class AzureRMRecoveryServicesVaultInfo(AzureRMModuleBaseExt): response = None self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) - changed = True + changed = False response = self.get_recovery_service_vault_info() self.results['response'] = response @@ -188,10 +198,12 @@ class AzureRMRecoveryServicesVaultInfo(AzureRMModuleBaseExt): self.log('Error in fetching Azure Recovery Service Vault Details.') self.fail('Error in fetching Azure Recovery Service Vault Details {0}'.format(str(e))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_rediscache.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_rediscache.py index b93a52992..5a231cc3e 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_rediscache.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_rediscache.py @@ -510,8 +510,7 @@ class AzureRMRedisCaches(AzureRMModuleBase): # get management client self._client = self.get_mgmt_svc_client(RedisManagementClient, base_url=self._cloud_environment.endpoints.resource_manager, - api_version='2018-03-01', - is_track2=True) + api_version='2018-03-01') # set location resource_group = self.get_resource_group(self.resource_group) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_rediscache_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_rediscache_info.py index 44e47f4d5..782fb0417 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_rediscache_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_rediscache_info.py @@ -269,8 +269,7 @@ class AzureRMRedisCacheInfo(AzureRMModuleBase): # get management client self._client = self.get_mgmt_svc_client(RedisManagementClient, base_url=self._cloud_environment.endpoints.resource_manager, - api_version='2018-03-01', - is_track2=True) + api_version='2018-03-01') if self.name: self.results['rediscaches'] = self.get_item() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_rediscachefirewallrule.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_rediscachefirewallrule.py index 67b2c90b6..b0fbdb43e 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_rediscachefirewallrule.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_rediscachefirewallrule.py @@ -176,8 +176,7 @@ class AzureRMRedisCacheFirewallRule(AzureRMModuleBase): # get management client self._client = self.get_mgmt_svc_client(RedisManagementClient, base_url=self._cloud_environment.endpoints.resource_manager, - api_version='2018-03-01', - is_track2=True) + api_version='2018-03-01') # check if the firewall rule exists old_response = self.get() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationassignment.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationassignment.py index a70542f80..8cffc7999 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationassignment.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationassignment.py @@ -141,11 +141,9 @@ class AzureRMRegistrationAssignment(AzureRMModuleBaseExt): ), properties=dict( type='dict', - disposition='/properties', options=dict( registration_definition_id=dict( type='str', - disposition='registration_definition_id', required=True ) ) @@ -188,7 +186,6 @@ class AzureRMRegistrationAssignment(AzureRMModuleBaseExt): self.mgmt_client = self.get_mgmt_svc_client(ManagedServicesClient, base_url=self._cloud_environment.endpoints.resource_manager, api_version='2019-09-01', - is_track2=True, suppress_subscription_id=True) old_response = self.get_resource() @@ -200,11 +197,9 @@ class AzureRMRegistrationAssignment(AzureRMModuleBaseExt): if self.state == 'absent': self.to_do = Actions.Delete else: - modifiers = {} - self.create_compare_modifiers(self.module_arg_spec, '', modifiers) - self.results['modifiers'] = modifiers - self.results['compare'] = [] - if not self.default_compare(modifiers, self.body, old_response, '', self.results): + if self.body.get('properties') is not None and \ + self.body['properties']['registration_definition_id'] != \ + old_response['properties']['registration_definition_id']: self.to_do = Actions.Update if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationassignment_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationassignment_info.py index ef8875f7c..2dabe949e 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationassignment_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationassignment_info.py @@ -126,7 +126,6 @@ class AzureRMRegistrationAssignmentInfo(AzureRMModuleBase): self.mgmt_client = self.get_mgmt_svc_client(ManagedServicesClient, base_url=self._cloud_environment.endpoints.resource_manager, api_version='2020-09-01', - is_track2=True, suppress_subscription_id=True) if (self.scope is not None and self.registration_assignment_id is not None): @@ -168,12 +167,14 @@ class AzureRMRegistrationAssignmentInfo(AzureRMModuleBase): def format_item(self, item): if hasattr(item, 'as_dict'): return [item.as_dict()] - else: + elif item is not None: result = [] items = list(item) for tmp in items: result.append(tmp.as_dict()) return result + else: + return [] def main(): diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationdefinition.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationdefinition.py index 0d89da3a9..0582df4cf 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationdefinition.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationdefinition.py @@ -261,63 +261,51 @@ class AzureRMRegistrationDefinition(AzureRMModuleBaseExt): ), properties=dict( type='dict', - disposition='/properties', options=dict( description=dict( type='str', - disposition='description' ), authorizations=dict( type='list', - disposition='authorizations', required=True, elements='dict', options=dict( principal_id=dict( type='str', - disposition='principal_id', required=True ), role_definition_id=dict( type='str', - disposition='role_definition_id', required=True ) ) ), registration_definition_name=dict( type='str', - disposition='registration_definition_name' ), managed_by_tenant_id=dict( type='str', - disposition='managed_by_tenant_id', required=True ) ) ), plan=dict( type='dict', - disposition='/plan', options=dict( name=dict( type='str', - disposition='name', required=True ), publisher=dict( type='str', - disposition='publisher', required=True ), product=dict( type='str', - disposition='product', required=True ), version=dict( type='str', - disposition='version', required=True ) ) @@ -349,8 +337,6 @@ class AzureRMRegistrationDefinition(AzureRMModuleBaseExt): elif kwargs[key] is not None: self.body[key] = kwargs[key] - self.inflate_parameters(self.module_arg_spec, self.body, 0) - if self.registration_definition_id is None: self.registration_definition_id = str(uuid.uuid4()) @@ -365,7 +351,6 @@ class AzureRMRegistrationDefinition(AzureRMModuleBaseExt): self.mgmt_client = self.get_mgmt_svc_client(ManagedServicesClient, base_url=self._cloud_environment.endpoints.resource_manager, api_version='2019-09-01', - is_track2=True, suppress_subscription_id=True) old_response = self.get_resource() @@ -377,11 +362,16 @@ class AzureRMRegistrationDefinition(AzureRMModuleBaseExt): if self.state == 'absent': self.to_do = Actions.Delete else: - modifiers = {} - self.create_compare_modifiers(self.module_arg_spec, '', modifiers) - self.results['modifiers'] = modifiers - self.results['compare'] = [] - if not self.default_compare(modifiers, self.body, old_response, '', self.results): + if self.body.get('plan') is not None: + if old_response.get('plan') is not None and \ + not all(self.body['plan'][item] == old_response['plan'].get(item) + for item in self.body['plan'].keys()): + self.to_do = Actions.Update + else: + self.to_do = Actions.Update + elif (self.body.get('properties') is not None and + not all(self.body['properties'][item] == old_response['properties'].get(item) + for item in self.body['properties'].keys())): self.to_do = Actions.Update if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationdefinition_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationdefinition_info.py index 1c095420c..cad5b8e2c 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationdefinition_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_registrationdefinition_info.py @@ -190,7 +190,6 @@ class AzureRMRegistrationDefinitionInfo(AzureRMModuleBase): self.mgmt_client = self.get_mgmt_svc_client(ManagedServicesClient, base_url=self._cloud_environment.endpoints.resource_manager, api_version='2019-09-01', - is_track2=True, suppress_subscription_id=True) if self.registration_definition_id is not None: @@ -223,12 +222,14 @@ class AzureRMRegistrationDefinitionInfo(AzureRMModuleBase): def format_item(self, item): if hasattr(item, 'as_dict'): return [item.as_dict()] - else: + elif item is not None: result = [] items = list(item) for tmp in items: result.append(tmp.as_dict()) return result + else: + return [] def main(): diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_resource.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_resource.py index a3a0544e2..4e786332f 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_resource.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_resource.py @@ -325,7 +325,6 @@ class AzureRMResource(AzureRMModuleBase): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if self.state == 'absent': @@ -416,10 +415,15 @@ class AzureRMResource(AzureRMModuleBase): self.polling_timeout, self.polling_interval) if self.state == 'present' and self.method != 'DELETE': - try: - response = json.loads(response.body()) - except Exception: + if hasattr(response, 'body'): + try: + response = json.loads(response.body()) + except Exception: + response = response.body() + elif hasattr(response, 'context'): response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) else: response = None diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_resource_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_resource_info.py index eb5cd93e4..176be9531 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_resource_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_resource_info.py @@ -76,6 +76,13 @@ options: description: - Subresource name. type: str + tags: + description: + - A dictionary of tags to filter on. + - Each key-value pair in the dictionary specifies a tag name and its value to filter on differente resources. + type: dict + required: false + default: {} extends_documentation_fragment: - azure.azcollection.azure @@ -98,6 +105,14 @@ EXAMPLES = ''' azure_rm_resource_info: resource_group: "{{ resource_group }}" resource_type: resources + +- name: Get all snapshots of all resource groups of a subscription but filtering with two tags. + azure_rm_resource_info: + provider: compute + resource_type: snapshots + tags: + enviroment: dev + department: hr ''' RETURN = ''' @@ -346,7 +361,8 @@ class AzureRMResourceInfo(AzureRMModuleBase): ), api_version=dict( type='str' - ) + ), + tags=dict(type='dict', default={}) ) # store the results of the module operation self.results = dict( @@ -370,7 +386,6 @@ class AzureRMResourceInfo(AzureRMModuleBase): for key in self.module_arg_spec: setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) if self.url is None: @@ -450,6 +465,12 @@ class AzureRMResourceInfo(AzureRMModuleBase): self.fail('Failed to parse response: ' + str(e)) if not skiptoken: break + if kwargs['tags']: + filtered_response = [] + for resource in self.results['response']: + if all(resource.get('tags', {}).get(tag_key) == tag_value for tag_key, tag_value in kwargs['tags'].items()): + filtered_response.append(resource) + self.results['response'] = filtered_response return self.results diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_roledefinition.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_roledefinition.py index 8683142d8..86f085099 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_roledefinition.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_roledefinition.py @@ -224,7 +224,6 @@ class AzureRMRoleDefinition(AzureRMModuleBase): # get management client self._client = self.get_mgmt_svc_client(AuthorizationManagementClient, base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True, api_version="2018-01-01-preview") self.scope = self.build_scope() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_roledefinition_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_roledefinition_info.py index ce8ee238e..8f7c9ac4d 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_roledefinition_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_roledefinition_info.py @@ -194,7 +194,6 @@ class AzureRMRoleDefinitionInfo(AzureRMModuleBase): # get management client self._client = self.get_mgmt_svc_client(AuthorizationManagementClient, base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True, api_version="2018-01-01-preview") if self.id: diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_securitygroup.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_securitygroup.py index 8b25449ca..bf247e216 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_securitygroup.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_securitygroup.py @@ -44,8 +44,11 @@ options: type: str choices: - Udp + - UDP - Tcp + - TCP - Icmp + - ICMP - "*" default: "*" source_port_range: @@ -164,8 +167,11 @@ options: type: str choices: - Udp + - UDP - Tcp + - TCP - Icmp + - ICMP - "*" default: "*" source_port_range: @@ -722,7 +728,7 @@ def create_network_security_group_dict(nsg): rule_spec = dict( name=dict(type='str', required=True), description=dict(type='str'), - protocol=dict(type='str', choices=['Udp', 'Tcp', 'Icmp', '*'], default='*'), + protocol=dict(type='str', choices=['Udp', 'UDP', 'Tcp', 'TCP', 'Icmp', 'ICMP', '*'], default='*'), source_port_range=dict(type='raw', default='*'), destination_port_range=dict(type='raw', default='*'), source_address_prefix=dict(type='raw', default='*'), diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_servicebus_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_servicebus_info.py index 5ccd4dea7..b736eb6c2 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_servicebus_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_servicebus_info.py @@ -378,6 +378,25 @@ servicebuses: "type": "Microsoft.ServiceBus/Namespaces/Queues/AuthorizationRules" } } + private_endpoint_connections: + description: + - Properties of the PrivateEndpointConnection. + type: list + returned: always + sample: [{ + "id": "/subscriptions/xxxxxx/resourceGroups/myRG/providers/Microsoft.ServiceBus/namespaces/fredVM/privateEndpointConnections/xxxxxxxx", + "name": "xxxxxx", + "private_endpoint": { + "id": "/subscriptions/xxxxx/resourceGroups/myRG/providers/Microsoft.Network/privateEndpoints/fredprivateendpoint" + }, + "private_link_service_connection_state": { + "description": "Auto-Approved", + "status": "Approved" + }, + "provisioning_state": "Succeeded", + "type": "Microsoft.ServiceBus/Namespaces/PrivateEndpointConnections" + }] + ''' try: @@ -385,7 +404,6 @@ try: except Exception: # This is handled in azure_rm_common pass - from ansible.module_utils.common.dict_transformations import _camel_to_snake from ansible.module_utils._text import to_native from datetime import datetime, timedelta @@ -484,6 +502,8 @@ class AzureRMServiceBusInfo(AzureRMModuleBase): result[attribute] = to_native(value) elif attribute == 'max_size_in_megabytes': result['max_size_in_mb'] = value + elif attribute == 'private_endpoint_connections': + result['private_endpoint_connections'] = [item.as_dict() for item in value] else: result[attribute] = value if self.show_sas_policies and self.type != 'subscription': diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_snapshot.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_snapshot.py index bd3acd9bd..b3db0a0d7 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_snapshot.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_snapshot.py @@ -142,24 +142,17 @@ class AzureRMSnapshots(AzureRMModuleBaseExt): self.module_arg_spec = dict( resource_group=dict( type='str', - updatable=False, - disposition='resourceGroupName', required=True ), name=dict( type='str', - updatable=False, - disposition='snapshotName', required=True ), location=dict( - type='str', - updatable=False, - disposition='/' + type='str' ), sku=dict( type='dict', - disposition='/', options=dict( name=dict( type='str', @@ -174,29 +167,22 @@ class AzureRMSnapshots(AzureRMModuleBaseExt): ), os_type=dict( type='str', - disposition='/properties/osType', choices=['Windows', 'Linux'] ), incremental=dict(type='bool', default=False), creation_data=dict( type='dict', - disposition='/properties/creationData', options=dict( create_option=dict( type='str', - disposition='createOption', choices=['Import', 'Copy'], ), source_uri=dict( - type='str', - disposition='sourceUri', - purgeIfNone=True + type='str' ), source_id=dict( - type='str', - disposition='sourceResourceId', - purgeIfNone=True + type='str' ) ) ), @@ -233,22 +219,27 @@ class AzureRMSnapshots(AzureRMModuleBaseExt): supports_tags=True) def exec_module(self, **kwargs): - for key in list(self.module_arg_spec.keys()): + for key in list(self.module_arg_spec.keys()) + ['tags']: if hasattr(self, key): setattr(self, key, kwargs[key]) elif kwargs[key] is not None: if key == 'incremental': self.body['properties']['incremental'] = kwargs[key] + elif key == 'os_type': + self.body['properties']['osType'] = kwargs[key] + elif key == 'creation_data': + self.body['properties']['creationData'] = dict() + if kwargs[key].get('create_option') is not None: + self.body['properties']['creationData']['createOption'] = kwargs[key].get('create_option') + self.body['properties']['creationData']['sourceUri'] = kwargs[key].get('source_uri') + self.body['properties']['creationData']['sourceResourceId'] = kwargs[key].get('source_id') else: self.body[key] = kwargs[key] - self.inflate_parameters(self.module_arg_spec, self.body, 0) - old_response = None response = None self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) resource_group = self.get_resource_group(self.resource_group) @@ -283,12 +274,22 @@ class AzureRMSnapshots(AzureRMModuleBaseExt): if self.state == 'absent': self.to_do = Actions.Delete else: - modifiers = {} - self.create_compare_modifiers(self.module_arg_spec, '', modifiers) - self.results['modifiers'] = modifiers - self.results['compare'] = [] - self.create_compare_modifiers(self.module_arg_spec, '', modifiers) - if not self.default_compare(modifiers, self.body, old_response, '', self.results): + if self.body.get('sku') is not None and \ + not all(self.body['sku'][item] == old_response['sku'].get(item) for item in self.body['sku'].keys()): + self.to_do = Actions.Update + if self.body['properties'].get('incremental') is not None and \ + self.body['properties']['incremental'] != old_response['properties']['incremental']: + self.to_do = Actions.Update + if self.body['properties'].get('osType') is not None and \ + self.body['properties']['osType'] != old_response['properties'].get('osType'): + self.to_do = Actions.Update + if self.body['properties'].get('creationData') is not None and \ + not all(self.body['properties']['creationData'][item] == old_response['properties']['creationData'].get(item) + for item in self.body['properties']['creationData'].keys()): + self.to_do = Actions.Update + + update_tags, self.body['tags'] = self.update_tags(old_response.get('tags')) + if update_tags: self.to_do = Actions.Update if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_sqlmanagedinstance.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_sqlmanagedinstance.py index b59421062..631931afd 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_sqlmanagedinstance.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_sqlmanagedinstance.py @@ -262,7 +262,7 @@ sql_managed_instance: sample: "/subscription/xxx-xxx/resourceGroups/testRG/providers/Microsoft.Sql/managedInstances/fredsqlinstance" name: description: - - SQL manged instance name. + - SQL managed instance name. returned: always type: str sample: testmanagedinstance diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_sqlmanagedinstance_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_sqlmanagedinstance_info.py index 5434cb6a4..3a85d00ac 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_sqlmanagedinstance_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_sqlmanagedinstance_info.py @@ -14,7 +14,7 @@ module: azure_rm_sqlmanagedinstance_info version_added: "0.15.0" short_description: Get Azure SQL managed instance facts description: - - Get facts of Azure SQL manged instance facts. + - Get facts of Azure SQL managed instance facts. options: resource_group: @@ -49,7 +49,7 @@ EXAMPLES = ''' azure_rm_sqlmanagedinstance_info: resource_group: testrg -- name: List SQL manged instance by subscription and filter by tags +- name: List SQL managed instance by subscription and filter by tags azure_rm_sqlmanagedinstance_info: tags: - foo @@ -70,7 +70,7 @@ sql_managed_instance: sample: "/subscription/xxx-xxx/resourceGroups/testRG/providers/Microsoft.Sql/managedInstances/fredsqlinstance" name: description: - - SQL manged instance name. + - SQL managed instance name. returned: always type: str sample: testmanagedinstance diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_sshpublickey.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_sshpublickey.py new file mode 100644 index 000000000..1c7a473b8 --- /dev/null +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_sshpublickey.py @@ -0,0 +1,266 @@ +#!/usr/bin/python +# +# Copyright (c) 2023 xuzhang3 (@xuzhang3), Fred-sun (@Fred-sun) +# +# 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 + + +DOCUMENTATION = ''' +--- +module: azure_rm_sshpublickey +version_added: "2.0.0" +short_description: Manage ssh public key with vm +description: + - Create, update or delete the vm public key. +options: + resource_group: + description: + - Name of resource group. + required: true + type: str + location: + description: + - Valid Azure location. Defaults to location of the resource group. + type: str + name: + description: + - The name of the SSH public key. + required: true + type: str + public_key: + description: + - SSH public key used to authenticate to a virtual machine through ssh. + - If this property is not initially provided when the resource is created, the publicKey property will be populated when generateKeyPair is called. + - If the public key is provided upon resource creation, the provided public key needs to be at least 2048-bit and in ssh-rsa format. + type: str + state: + description: + - State of the SSH Public Key. Use C(present) to create or update and C(absent) to delete. + default: present + type: str + choices: + - absent + - present + +extends_documentation_fragment: + - azure.azcollection.azure + - azure.azcollection.azure_tags + +author: + - xuzhang3 (@xuzhang3) + - Fred-sun (@Fred-sun) + +''' + +EXAMPLES = ''' +- name: Create a SSH Public Key + azure_rm_sshpublickey: + resource_group: myResourceGroup + name: mySshPublicKey + public_key: "ssh-rsa ****************************@test.com" + tags: + testing: testing + delete: on-exit + +- name: Generate a pair SSH Public Key + azure_rm_sshpublickey: + resource_group: myResourceGroup + name: mySshPublicKey + tags: + testing: testing + delete: on-exit + +- name: Delete a SSH Public Key + azure_rm_sshpublickey: + resource_group: myResourceGroup + name: mySshPublicKey + state: absent +''' +RETURN = ''' +state: + description: + - Current state of the SSH Public Key. + returned: always + type: complex + contains: + id: + description: + - Resource ID. + returned: always + type: str + sample: /subscriptions/xxxx/resourceGroups/xxx/providers/Microsoft.Compute/sshPublicKeys/mySshPublicKeyName + location: + description: + - The Geo-location where the resource lives. + returned: always + type: str + sample: eastus + name: + description: + - Resource name. + returned: always + type: str + sample: mySshPublicKey + tags: + description: + - Resource tags, such as { 'tags1':'value1' }. + returned: always + type: dict + sample: { 'key1':'value1' } + public_key: + description: + - SSH public key used to authenticate to a virtual machine through ssh. + returned: always + type: str + sample: "ssh-rsa **************@test.com" +''' + +try: + from azure.core.exceptions import ResourceNotFoundError +except ImportError: + # This is handled in azure_rm_common + pass + +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + + +class AzureRMSshPublicKey(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + resource_group=dict(type='str', required=True), + name=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', 'absent']), + location=dict(type='str'), + public_key=dict(type='str'), + ) + + self.resource_group = None + self.name = None + self.state = None + self.location = None + self.public_key = None + + self.body = dict() + + self.results = dict( + changed=False, + state=dict() + ) + + super(AzureRMSshPublicKey, self).__init__(self.module_arg_spec, + supports_tags=True, + supports_check_mode=True) + + def exec_module(self, **kwargs): + + for key in list(self.module_arg_spec.keys()) + ['tags']: + setattr(self, key, kwargs[key]) + for key in ['tags', 'public_key']: + self.body[key] = kwargs[key] + + resource_group = self.get_resource_group(self.resource_group) + if not self.location: + # Set default location + self.location = resource_group.location + + changed = False + results = dict() + + old_response = self.get_by_name() + + if old_response is not None: + if self.state == 'present': + update_tags, self.body['tags'] = self.update_tags(old_response['tags']) + if update_tags or self.body['public_key'] is not None and self.body['public_key'] != old_response['public_key']: + changed = True + if not self.check_mode: + results = self.update_ssh_public_key(self.body) + else: + results = old_response + else: + changed = True + if not self.check_mode: + results = self.delete_ssh_public_key() + else: + if self.state == 'present': + changed = True + if not self.check_mode: + self.body['location'] = self.location + results = self.create_ssh_public_key(self.body) + else: + changed = False + self.log("The SSH Public Key is not exists") + + self.results['changed'] = changed + self.results['state'] = results + + return self.results + + def get_by_name(self): + response = None + try: + response = self.compute_client.ssh_public_keys.get(self.resource_group, self.name) + + except ResourceNotFoundError as exec: + self.log("Failed to get ssh public keys, Exception as {0}".format(exec)) + + return self.to_dict(response) + + def create_ssh_public_key(self, body): + response = None + try: + if body.get('public_key') is None: + response = self.to_dict(self.compute_client.ssh_public_keys.create(self.resource_group, self.name, body)) + response.update(self.to_dict(self.compute_client.ssh_public_keys.generate_key_pair(self.resource_group, self.name))) + else: + response = self.to_dict(self.compute_client.ssh_public_keys.create(self.resource_group, self.name, body)) + except Exception as exc: + self.fail("Error creating SSH Public Key {0} - {1}".format(self.name, str(exc))) + + return response + + def update_ssh_public_key(self, body): + response = None + try: + response = self.compute_client.ssh_public_keys.update(self.resource_group, self.name, body) + except Exception as exc: + self.fail("Error updating SSH Public Key {0} - {1}".format(self.name, str(exc))) + return self.to_dict(response) + + def delete_ssh_public_key(self): + try: + self.compute_client.ssh_public_keys.delete(self.resource_group, self.name) + except Exception as exc: + self.fail("Error deleting SSH Public Key {0} - {1}".format(self.name, str(exc))) + + def to_dict(self, body): + results = None + if body is not None: + if hasattr(body, 'private_key'): + results = dict( + private_key=body.private_key, + id=body.id, + public_key=body.public_key + ) + else: + results = dict( + id=body.id, + name=body.name, + location=body.location, + tags=body.tags, + public_key=body.public_key + ) + return results + + +def main(): + AzureRMSshPublicKey() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_sshpublickey_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_sshpublickey_info.py new file mode 100644 index 000000000..3a84f2e74 --- /dev/null +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_sshpublickey_info.py @@ -0,0 +1,196 @@ +#!/usr/bin/python +# +# Copyright (c) 2023 xuzhang3 (@xuzhang3), Fred-sun (@Fred-sun) +# +# 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 + + +DOCUMENTATION = ''' +--- +module: azure_rm_sshpublickey_info + +version_added: "2.0.0" + +short_description: Get Ssh Public Key with VM facts + +description: + - Get Ssh Public Key with VM facts + +options: + resource_group: + description: + - Name of the resource group. + type: str + name: + description: + - Name of the SSH Public Key. + type: str + tags: + description: + - Limit results by providing a list of tags. Format tags as 'key' or 'key:value'. + type: list + elements: str + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - xuzhang3 (@xuzhang3) + - Fred-sun (@Fred-sun) + +''' + +EXAMPLES = ''' +- name: Get facts of the VM's ssh public key by name + azure_rm_sshpublickey_info: + resource_group: myResourceGroup + name: mysshpublickey + +- name: Get facts of the VM's ssh public key by resource group + azure_rm_sshpublickey_info: + resource_group: myResourceGroup + +- name: Get facts by tags + azure_rm_sshpublickey_info: + resource_group: myResourceGroup + tags: + - testing + - foo:bar +''' + +RETURN = ''' +ssh_keys: + description: + - Current state of the SSH Public Key. + returned: always + type: complex + contains: + id: + description: + - Resource ID. + returned: always + type: str + sample: /subscriptions/xxxx/resourceGroups/xxx/providers/Microsoft.Compute/sshPublicKeys/mySshPublicKeyName + location: + description: + - The Geo-location where the resource lives. + returned: always + type: str + sample: eastus + name: + description: + - Resource name. + returned: always + type: str + sample: mySshPublicKey + tags: + description: + - Resource tags, such as { 'tags1':'value1' }. + returned: always + type: dict + sample: { 'key1':'value1' } + public_key: + description: + - SSH public key used to authenticate to a virtual machine through ssh. + returned: always + type: str + sample: "ssh-rsa **************@test.com" +''' + +try: + from azure.core.exceptions import ResourceNotFoundError +except Exception: + # This is handled in azure_rm_common + pass + +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + + +class AzureRMSshPublicKeyInfo(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + resource_group=dict(type='str'), + name=dict(type='str'), + tags=dict(type='list', elements='str') + ) + + self.results = dict( + changed=False, + ssh_keys=[] + ) + + self.resource_group = None + self.name = None + self.tags = None + + super(AzureRMSshPublicKeyInfo, self).__init__(self.module_arg_spec, + supports_check_mode=True, + 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 self.resource_group: + response = [self.get_by_name()] + elif self.resource_group: + response = self.list_by_resourcegroup() + else: + response = self.list_all() + + self.results['ssh_keys'] = [self.to_dict(item) for item in response if response is not None] + + return self.results + + def get_by_name(self): + response = None + try: + response = self.compute_client.ssh_public_keys.get(self.resource_group, self.name) + + except ResourceNotFoundError as exec: + self.log("Failed to get ssh public keys, Exception as {0}".format(exec)) + + return response + + def list_by_resourcegroup(self): + response = None + try: + response = self.compute_client.ssh_public_keys.list_by_resource_group(self.resource_group) + except Exception as exec: + self.log("Faild to list ssh public keys by resource group, exception as {0}".format(exec)) + return response + + def list_all(self): + response = None + try: + response = self.compute_client.ssh_public_keys.list_by_subscription() + except Exception as exc: + self.fail("Failed to list all items - {0}".format(str(exc))) + + return response + + def to_dict(self, body): + results = dict() + if body is not None and self.has_tags(body.tags, self.tags): + results = dict( + id=body.id, + name=body.name, + location=body.location, + tags=body.tags, + public_key=body.public_key + ) + return results + + +def main(): + AzureRMSshPublicKeyInfo() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_storageaccount.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_storageaccount.py index c7c280ef4..42c5b2e42 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_storageaccount.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_storageaccount.py @@ -87,6 +87,10 @@ options: - Account HierarchicalNamespace enabled if sets to true. - When I(is_hns_enabled=True), I(kind) cannot be C(Storage). type: bool + enable_nfs_v3: + description: + - NFS 3.0 protocol. + type: bool access_tier: description: - The access tier for this storage account. Required when I(kind=BlobStorage). @@ -240,6 +244,13 @@ options: description: - The absolute path of the custom 404 page. type: str + large_file_shares_state: + description: + - Allow large file shares if sets to Enabled. + type: str + choices: + - Enabled + - Disabled encryption: description: - The encryption settings on the storage account. @@ -333,6 +344,16 @@ EXAMPLES = ''' tags: testing: testing +- name: Create storage account with I(enable_nfs_v3=false) + azure_rm_storageaccount: + resource_group: myResourceGroup + name: c1h0002 + account_type: Premium_LRS + kind: FileStorage + enable_nfs_v3: false + static_website: + enabled: true + - name: configure firewall and virtual networks azure_rm_storageaccount: resource_group: myResourceGroup @@ -462,6 +483,12 @@ state: type: bool returned: always sample: true + enable_nfs_v3: + description: + - NFS 3.0 protocol. + type: bool + returned: always + sample: false location: description: - Valid Azure location. Defaults to location of the resource group. @@ -599,6 +626,12 @@ state: returned: always type: str sample: "Microsoft.Storage/storageAccounts" + large_file_shares_state: + description: + - Allow large file shares if sets to Enabled. + type: str + returned: always + sample: Enabled static_website: description: - Static website configuration for the storage account. @@ -711,6 +744,8 @@ class AzureRMStorageAccount(AzureRMModuleBase): blob_cors=dict(type='list', options=cors_rule_spec, elements='dict'), static_website=dict(type='dict', options=static_website_spec), is_hns_enabled=dict(type='bool'), + large_file_shares_state=dict(type='str', choices=['Enabled', 'Disabled']), + enable_nfs_v3=dict(type='bool'), encryption=dict( type='dict', options=dict( @@ -766,6 +801,8 @@ class AzureRMStorageAccount(AzureRMModuleBase): self.static_website = None self.encryption = None self.is_hns_enabled = None + self.large_file_shares_state = None + self.enable_nfs_v3 = None super(AzureRMStorageAccount, self).__init__(self.module_arg_spec, supports_check_mode=True) @@ -870,6 +907,8 @@ class AzureRMStorageAccount(AzureRMModuleBase): allow_blob_public_access=account_obj.allow_blob_public_access, network_acls=account_obj.network_rule_set, is_hns_enabled=account_obj.is_hns_enabled if account_obj.is_hns_enabled else False, + enable_nfs_v3=account_obj.enable_nfs_v3 if hasattr(account_obj, 'enable_nfs_v3') else None, + large_file_shares_state=account_obj.large_file_shares_state, static_website=dict( enabled=False, index_document=None, @@ -1020,6 +1059,10 @@ class AzureRMStorageAccount(AzureRMModuleBase): self.results['changed'] = True self.update_network_rule_set() + if self.enable_nfs_v3 is not None and bool(self.enable_nfs_v3) != bool(self.account_dict.get('enable_nfs_v3')): + self.results['changed'] = True + self.account_dict['enable_nfs_v3'] = self.enable_nfs_v3 + if self.is_hns_enabled is not None and bool(self.is_hns_enabled) != bool(self.account_dict.get('is_hns_enabled')): self.results['changed'] = True self.account_dict['is_hns_enabled'] = self.is_hns_enabled @@ -1129,6 +1172,21 @@ class AzureRMStorageAccount(AzureRMModuleBase): except Exception as exc: self.fail("Failed to update access tier: {0}".format(str(exc))) + if self.large_file_shares_state is not None: + if self.large_file_shares_state != self.account_dict['large_file_shares_state']: + self.results['changed'] = True + self.account_dict['large_file_shares_state'] = self.large_file_shares_state + + if self.results['changed'] and not self.check_mode: + if self.large_file_shares_state == 'Disabled': + parameters = self.storage_models.StorageAccountUpdateParameters(large_file_shares_state=None) + else: + parameters = self.storage_models.StorageAccountUpdateParameters(large_file_shares_state=self.large_file_shares_state) + try: + self.storage_client.storage_accounts.update(self.resource_group, self.name, parameters) + except Exception as exc: + self.fail("Failed to update large_file_shares_state: {0}".format(str(exc))) + update_tags, self.account_dict['tags'] = self.update_tags(self.account_dict['tags']) if update_tags: self.results['changed'] = True @@ -1198,6 +1256,8 @@ class AzureRMStorageAccount(AzureRMModuleBase): allow_blob_public_access=self.allow_blob_public_access, encryption=self.encryption, is_hns_enabled=self.is_hns_enabled, + enable_nfs_v3=self.enable_nfs_v3, + large_file_shares_state=self.large_file_shares_state, tags=dict() ) if self.tags: @@ -1223,7 +1283,9 @@ class AzureRMStorageAccount(AzureRMModuleBase): allow_blob_public_access=self.allow_blob_public_access, encryption=self.encryption, is_hns_enabled=self.is_hns_enabled, - access_tier=self.access_tier) + enable_nfs_v3=self.enable_nfs_v3, + access_tier=self.access_tier, + large_file_shares_state=self.large_file_shares_state) self.log(str(parameters)) try: poller = self.storage_client.storage_accounts.begin_create(self.resource_group, self.name, parameters) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_storageaccount_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_storageaccount_info.py index 75f179b6f..a530459dd 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_storageaccount_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_storageaccount_info.py @@ -211,6 +211,12 @@ storageaccounts: type: bool returned: always sample: true + enable_nfs_v3: + description: + - NFS 3.0 protocol. + type: bool + returned: always + sample: false kind: description: - The kind of storage. @@ -509,6 +515,12 @@ storageaccounts: returned: always type: dict sample: { "tag1": "abc" } + large_file_shares_state: + description: + - Allow large file shares if sets to Enabled. + type: str + returned: always + sample: Enabled static_website: description: - Static website configuration for the storage account. @@ -674,6 +686,8 @@ class AzureRMStorageAccountInfo(AzureRMModuleBase): public_network_access=account_obj.public_network_access, allow_blob_public_access=account_obj.allow_blob_public_access, is_hns_enabled=account_obj.is_hns_enabled if account_obj.is_hns_enabled else False, + large_file_shares_state=account_obj.large_file_shares_state, + enable_nfs_v3=account_obj.enable_nfs_v3 if hasattr(account_obj, 'enable_nfs_v3') else None, static_website=dict( enabled=False, index_document=None, diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_storageblob.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_storageblob.py index 868069f42..ad54b8422 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_storageblob.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_storageblob.py @@ -22,6 +22,17 @@ description: - the module can work exclusively in three modes, when C(batch_upload_src) is set, it is working in batch upload mode; when C(src) is set, it is working in upload mode and when C(dst) is set, it is working in dowload mode. options: + auth_mode: + description: + - The mode in which to run the command. C(login) mode will directly use your login credentials for the authentication. + - The legacy C(key) mode will attempt to query for an account key if no authentication parameters for the account are provided. + - Can also be set via the environment variable C(AZURE_STORAGE_AUTH_MODE). + default: key + type: str + choices: + - key + - login + version_added: "1.19.0" storage_account_name: description: - Name of the storage account to use. @@ -214,6 +225,7 @@ except ImportError: pass from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase +from ansible.module_utils.basic import env_fallback class AzureRMStorageBlob(AzureRMModuleBase): @@ -221,6 +233,12 @@ class AzureRMStorageBlob(AzureRMModuleBase): def __init__(self): self.module_arg_spec = dict( + auth_mode=dict( + type='str', + choices=['key', 'login'], + fallback=(env_fallback, ['AZURE_STORAGE_AUTH_MODE']), + default="key" + ), storage_account_name=dict(required=True, type='str', aliases=['account_name', 'storage_account']), blob=dict(type='str', aliases=['blob_name']), blob_type=dict(type='str', default='block', choices=['block', 'page']), @@ -281,7 +299,7 @@ class AzureRMStorageBlob(AzureRMModuleBase): # add file path validation - self.blob_service_client = self.get_blob_service_client(self.resource_group, self.storage_account_name) + self.blob_service_client = self.get_blob_service_client(self.resource_group, self.storage_account_name, self.auth_mode) self.container_obj = self.get_container() if self.blob: self.blob_obj = self.get_blob() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_subnet.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_subnet.py index ec7117c37..3fbe2b803 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_subnet.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_subnet.py @@ -124,6 +124,7 @@ options: required: True type: str choices: + - Microsoft.ContainerService/managedClusters - Microsoft.Web/serverFarms - Microsoft.ContainerInstance/containerGroups - Microsoft.Netapp/volumes @@ -352,7 +353,8 @@ delegations_spec = dict( 'Microsoft.DBforPostgreSQL/serversv2', 'Microsoft.AzureCosmosDB/clusters', 'Microsoft.MachineLearningServices/workspaces', 'Microsoft.DBforPostgreSQL/singleServers', 'Microsoft.DBforPostgreSQL/flexibleServers', 'Microsoft.DBforMySQL/serversv2', 'Microsoft.DBforMySQL/flexibleServers', 'Microsoft.ApiManagement/service', 'Microsoft.Synapse/workspaces', - 'Microsoft.PowerPlatform/vnetaccesslinks', 'Microsoft.Network/managedResolvers', 'Microsoft.Kusto/clusters'] + 'Microsoft.PowerPlatform/vnetaccesslinks', 'Microsoft.Network/managedResolvers', 'Microsoft.Kusto/clusters', + 'Microsoft.ContainerService/managedClusters'] ), actions=dict( type='list', diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachine.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachine.py index 35435b821..e845e2fa1 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachine.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachine.py @@ -229,6 +229,10 @@ options: description: - Size of OS disk in GB. type: int + os_disk_encryption_set: + description: + - ID of disk encryption set for OS disk. + type: str os_type: description: - Base type of operating system. @@ -245,7 +249,7 @@ options: data_disks: description: - Describes list of data disks. - - Use M(azure.azcollection.azure_rm_mangeddisk) to manage the specific disk. + - Use M(azure.azcollection.azure_rm_manageddisk) to manage the specific disk. type: list elements: dict suboptions: @@ -262,6 +266,10 @@ options: - Size can be changed only when the virtual machine is deallocated. - Not sure when I(managed_disk_id) defined. type: int + disk_encryption_set: + description: + - ID of disk encryption set for data disk. + type: str managed_disk_type: description: - Managed data disk type. @@ -409,6 +417,7 @@ options: zones: description: - A list of Availability Zones for your VM. + - A maximum of one zone can be configured. type: list elements: str license_type: @@ -569,6 +578,33 @@ options: description: - Specifies whether vTPM should be enabled on the virtual machine. type: bool + swap_os_disk: + description: + - The swap OS disk parameters. + type: dict + suboptions: + os_disk_id: + description: + - The swap OS disk's ID. + type: str + os_disk_name: + description: + - The swap OS disk's name. + type: str + os_disk_resource_group: + description: + - The swap OS disk's resource group. + type: str + additional_capabilities: + description: + - Enables or disables a capability on the virtual machine. + type: dict + suboptions: + ultra_ssd_enabled: + description: + - The flag that enables or disables a capability to have one or more managed data disks with UltraSSD_LRS storage account type on the VM. + - Managed disks with storage account type UltraSSD_LRS can be added to a virtual machine set only if this property is enabled. + type: bool extends_documentation_fragment: - azure.azcollection.azure @@ -760,6 +796,26 @@ EXAMPLES = ''' sku: '7.1' version: latest +- name: Update VM to swap OS disk + azure_rm_virtualmachine: + resource_group: "{{ resource_group }}" + name: "vmforimage{{ rpfx }}" + admin_username: testuser + ssh_password_enabled: false + managed_disk_type: Premium_LRS + ssh_public_keys: + - path: /home/testuser/.ssh/authorized_keys + key_data: "ssh-rsa *******************@qq.com" + vm_size: Standard_D4s_v3 + swap_os_disk: + os_disk_name: "{{ os_disk_name }}" + os_disk_resource_group: "{{ os_disk_resource_group }}" + image: + offer: 0001-com-ubuntu-server-focal + publisher: Canonical + sku: 20_04-lts + version: latest + - name: Power Off azure_rm_virtualmachine: resource_group: myResourceGroup @@ -856,6 +912,9 @@ azure_vm: type: dict sample: { "properties": { + "additional_capabilities": { + "ultra_ssd_enabled": false + }, "availabilitySet": { "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroup/myResourceGroup/providers/Microsoft.Compute/availabilitySets/MYAVAILABILITYSET" }, @@ -1088,6 +1147,7 @@ class AzureRMVirtualMachine(AzureRMModuleBase): storage_blob_name=dict(type='str', aliases=['storage_blob']), os_disk_caching=dict(type='str', aliases=['disk_caching'], choices=['ReadOnly', 'ReadWrite']), os_disk_size_gb=dict(type='int'), + os_disk_encryption_set=dict(type='str'), managed_disk_type=dict(type='str', choices=['Standard_LRS', 'StandardSSD_LRS', 'StandardSSD_ZRS', 'Premium_LRS', 'Premium_ZRS', 'UltraSSD_LRS']), os_disk_name=dict(type='str'), proximity_placement_group=dict(type='dict', options=proximity_placement_group_spec), @@ -1106,18 +1166,27 @@ class AzureRMVirtualMachine(AzureRMModuleBase): started=dict(type='bool'), force=dict(type='bool', default=False), generalized=dict(type='bool', default=False), + swap_os_disk=dict( + type='dict', + options=dict( + os_disk_resource_group=dict(type='str'), + os_disk_name=dict(type='str'), + os_disk_id=dict(type='str') + ) + ), data_disks=dict( type='list', elements='dict', options=dict( lun=dict(type='int', required=True), disk_size_gb=dict(type='int'), + disk_encryption_set=dict(type='str'), managed_disk_type=dict(type='str', choices=['Standard_LRS', 'StandardSSD_LRS', 'StandardSSD_ZRS', 'Premium_LRS', 'Premium_ZRS', 'UltraSSD_LRS']), storage_account_name=dict(type='str'), storage_container_name=dict(type='str', default='vhds'), storage_blob_name=dict(type='str'), - caching=dict(type='str', choices=['ReadOnly', 'ReadOnly']) + caching=dict(type='str', choices=['ReadOnly', 'ReadWrite']) ) ), plan=dict(type='dict'), @@ -1149,6 +1218,12 @@ class AzureRMVirtualMachine(AzureRMModuleBase): windows_config=dict(type='dict', options=windows_configuration_spec), linux_config=dict(type='dict', options=linux_configuration_spec), security_profile=dict(type='dict'), + additional_capabilities=dict( + type='dict', + options=dict( + ultra_ssd_enabled=dict(type='bool') + ) + ) ) self.resource_group = None @@ -1172,6 +1247,7 @@ class AzureRMVirtualMachine(AzureRMModuleBase): self.os_type = None self.os_disk_caching = None self.os_disk_size_gb = None + self.os_disk_encryption_set = None self.managed_disk_type = None self.os_disk_name = None self.proximity_placement_group = None @@ -1201,6 +1277,8 @@ class AzureRMVirtualMachine(AzureRMModuleBase): self.linux_config = None self.windows_config = None self.security_profile = None + self.additional_capabilities = None + self.swap_os_disk = None self.results = dict( changed=False, @@ -1209,8 +1287,10 @@ class AzureRMVirtualMachine(AzureRMModuleBase): ansible_facts=dict(azure_vm=None) ) + required_if = [('os_disk_encryption_set', '*', ['managed_disk_type'])] + super(AzureRMVirtualMachine, self).__init__(derived_arg_spec=self.module_arg_spec, - supports_check_mode=True) + supports_check_mode=True, required_if=required_if) @property def boot_diagnostics_present(self): @@ -1277,12 +1357,16 @@ class AzureRMVirtualMachine(AzureRMModuleBase): vm_dict = None image_reference = None custom_image = False + swap_os_disk_flag = False resource_group = self.get_resource_group(self.resource_group) if not self.location: # Set default location self.location = resource_group.location + if self.swap_os_disk is not None: + swap_os_disk = self.get_os_disk(self.swap_os_disk) + self.location = normalize_location_name(self.location) if self.state == 'present': @@ -1411,6 +1495,12 @@ class AzureRMVirtualMachine(AzureRMModuleBase): vm_dict['network_profile']['network_interfaces'] = updated_nics changed = True + if self.swap_os_disk is not None and swap_os_disk.get('name') != vm_dict['storage_profile']['os_disk']['name']: + self.log('CHANGED: Swap virtual machine {0} - OS disk name'.format(self.name)) + differences.append('Swap OS Disk') + changed = True + swap_os_disk_flag = True + if self.os_disk_caching and \ self.os_disk_caching != vm_dict['storage_profile']['os_disk']['caching']: self.log('CHANGED: virtual machine {0} - OS disk caching'.format(self.name)) @@ -1448,7 +1538,7 @@ class AzureRMVirtualMachine(AzureRMModuleBase): self.log('CHANGED: virtual machine {0} - short hostname'.format(self.name)) differences.append('Short Hostname') changed = True - vm_dict['os_orofile']['computer_name'] = self.short_hostname + vm_dict['os_profile']['computer_name'] = self.short_hostname if self.started and vm_dict['powerstate'] not in ['starting', 'running'] and self.allocated: self.log("CHANGED: virtual machine {0} not running and requested state 'running'".format(self.name)) @@ -1479,7 +1569,7 @@ class AzureRMVirtualMachine(AzureRMModuleBase): differences.append('Zones') changed = True - if self.license_type is not None and vm_dict.get('licenseType') != self.license_type: + if self.license_type is not None and vm_dict.get('license_type') != self.license_type: differences.append('License Type') changed = True @@ -1516,7 +1606,7 @@ class AzureRMVirtualMachine(AzureRMModuleBase): if self.security_profile is not None: update_security_profile = False - if 'securityProfile' not in vm_dict.keys(): + if 'security_profile' not in vm_dict.keys(): update_security_profile = True differences.append('security_profile') else: @@ -1547,6 +1637,12 @@ class AzureRMVirtualMachine(AzureRMModuleBase): changed = True differences.append('security_profile') + if self.additional_capabilities is not None: + if 'additional_capabilities' not in vm_dict.keys() or\ + bool(self.additional_capabilities['ultra_ssd_enabled']) != bool(vm_dict['additional_capabilities']['ultra_ssd_enabled']): + changed = True + differences.append('additional_capabilities') + if self.windows_config is not None and vm_dict['os_profile'].get('windows_configuration') is not None: if self.windows_config['enable_automatic_updates'] != vm_dict['os_profile']['windows_configuration']['enable_automatic_updates']: self.fail("(PropertyChangeNotAllowed) Changing property 'windowsConfiguration.enableAutomaticUpdates' is not allowed.") @@ -1559,6 +1655,11 @@ class AzureRMVirtualMachine(AzureRMModuleBase): vm_dict['os_profile']['linux_configuration']['disable_password_authentication']: self.fail("(PropertyChangeNotAllowed) Changing property 'linuxConfiguration.disablePasswordAuthentication' is not allowed.") + current_os_des_id = vm_dict['storage_profile'].get('os_disk', {}).get('managed_disk', {}).get('disk_encryption_set', {}).get('id', None) + if self.os_disk_encryption_set is not None and current_os_des_id is not None: + if self.os_disk_encryption_set != current_os_des_id: + self.fail("(PropertyChangeNotAllowed) Changing property 'storage_profile.os_disk.managed_disk.disk_encryption_set' is not allowed.") + # Defaults for boot diagnostics if 'diagnostics_profile' not in vm_dict: vm_dict['diagnostics_profile'] = {} @@ -1601,6 +1702,29 @@ class AzureRMVirtualMachine(AzureRMModuleBase): vm_dict['tags']['_own_sa_'] = own_sa changed = True + if self.proximity_placement_group is not None: + if vm_dict.get('proximity_placement_group') is None: + changed = True + differences.append('proximity_placement_group') + if self.proximity_placement_group.get('name') is not None and self.proximity_placement_group.get('resource_group') is not None: + proximity_placement_group = self.get_proximity_placement_group(self.proximity_placement_group.get('resource_group'), + self.proximity_placement_group.get('name')) + self.proximity_placement_group['id'] = proximity_placement_group.id + + elif self.proximity_placement_group.get('id') is not None: + if vm_dict['proximity_placement_group'].get('id', "").lower() != self.proximity_placement_group['id'].lower(): + changed = True + differences.append('proximity_placement_group') + elif self.proximity_placement_group.get('name') is not None and self.proximity_placement_group.get('resource_group') is not None: + proximity_placement_group = self.get_proximity_placement_group(self.proximity_placement_group.get('resource_group'), + self.proximity_placement_group.get('name')) + if vm_dict['proximity_placement_group'].get('id', "").lower() != proximity_placement_group.id.lower(): + changed = True + differences.append('proximity_placement_group') + self.proximity_placement_group['id'] = proximity_placement_group.id + else: + self.fail("Parameter error: Please recheck your proximity placement group ") + self.differences = differences elif self.state == 'absent': @@ -1700,6 +1824,11 @@ class AzureRMVirtualMachine(AzureRMModuleBase): vhd = self.compute_models.VirtualHardDisk(uri=requested_vhd_uri) managed_disk = None + if managed_disk and self.os_disk_encryption_set: + managed_disk.disk_encryption_set = self.compute_models.DiskEncryptionSetParameters( + id=self.os_disk_encryption_set + ) + plan = None if self.plan: plan = self.compute_models.Plan(name=self.plan.get('name'), product=self.plan.get('product'), @@ -1865,6 +1994,10 @@ class AzureRMVirtualMachine(AzureRMModuleBase): else: data_disk_vhd = None data_disk_managed_disk = self.compute_models.ManagedDiskParameters(storage_account_type=data_disk['managed_disk_type']) + if data_disk.get('disk_encryption_set'): + data_disk_managed_disk.disk_encryption_set = self.compute_models.DiskEncryptionSetParameters( + id=data_disk['disk_encryption_set'] + ) disk_name = self.name + "-datadisk-" + str(count) count += 1 @@ -1916,6 +2049,12 @@ class AzureRMVirtualMachine(AzureRMModuleBase): ) vm_resource.security_profile = security_profile + if self.additional_capabilities is not None: + additional_capabilities = self.compute_models.AdditionalCapabilities( + ultra_ssd_enabled=self.additional_capabilities.get('ultra_ssd_enabled') + ) + vm_resource.additional_capabilities = additional_capabilities + self.log("Create virtual machine with parameters:") self.create_or_update_vm(vm_resource, 'all_autocreated' in self.remove_on_absent) @@ -1938,11 +2077,18 @@ class AzureRMVirtualMachine(AzureRMModuleBase): ) proximity_placement_group_resource = None - try: - proximity_placement_group_resource = self.compute_models.SubResource(id=vm_dict['proximity_placement_group'].get('id')) - except Exception: - # pass if the proximity Placement Group - pass + if self.proximity_placement_group is not None: + try: + proximity_placement_group_resource = self.compute_models.SubResource(id=self.proximity_placement_group.get('id')) + except Exception: + # pass if the proximity Placement Group + pass + else: + try: + proximity_placement_group_resource = self.compute_models.SubResource(id=vm_dict['proximity_placement_group'].get('id')) + except Exception: + # pass if the proximity Placement Group + pass availability_set_resource = None try: @@ -1984,7 +2130,24 @@ class AzureRMVirtualMachine(AzureRMModuleBase): hardware_profile=self.compute_models.HardwareProfile( vm_size=vm_dict['hardware_profile'].get('vm_size') ), - storage_profile=self.compute_models.StorageProfile( + availability_set=availability_set_resource, + proximity_placement_group=proximity_placement_group_resource, + network_profile=self.compute_models.NetworkProfile( + network_interfaces=nics + ) + ) + + if swap_os_disk_flag: + storage_profile = self.compute_models.StorageProfile( + os_disk=self.compute_models.OSDisk( + name=swap_os_disk.get('name'), + managed_disk=swap_os_disk.get('managed_disk'), + create_option=vm_dict['storage_profile']['os_disk'].get('create_option'), + ), + image_reference=image_reference + ) + else: + storage_profile = self.compute_models.StorageProfile( os_disk=self.compute_models.OSDisk( name=vm_dict['storage_profile']['os_disk'].get('name'), vhd=vhd, @@ -1995,13 +2158,9 @@ class AzureRMVirtualMachine(AzureRMModuleBase): disk_size_gb=vm_dict['storage_profile']['os_disk'].get('disk_size_gb') ), image_reference=image_reference - ), - availability_set=availability_set_resource, - proximity_placement_group=proximity_placement_group_resource, - network_profile=self.compute_models.NetworkProfile( - network_interfaces=nics ) - ) + + vm_resource.storage_profile = storage_profile if self.license_type is not None: vm_resource.license_type = self.license_type @@ -2083,8 +2242,8 @@ class AzureRMVirtualMachine(AzureRMModuleBase): ) else: vm_resource.os_profile.windows_configuration = self.compute_models.WindowsConfiguration( - provision_vm_agent=windows_config.get('provisionVMAgent', True), - enable_automatic_updates=windows_config.get('enableAutomaticUpdates', True) + provision_vm_agent=windows_config.get('provision_vm_agent', True), + enable_automatic_updates=windows_config.get('enable_automatic_updates', True) ) # Add linux configuration, if applicable @@ -2100,12 +2259,12 @@ class AzureRMVirtualMachine(AzureRMModuleBase): ) ssh_config = linux_config.get('ssh', None) if ssh_config: - public_keys = ssh_config.get('publicKeys') + public_keys = ssh_config.get('public_keys') if public_keys: vm_resource.os_profile.linux_configuration.ssh = self.compute_models.SshConfiguration(public_keys=[]) for key in public_keys: vm_resource.os_profile.linux_configuration.ssh.public_keys.append( - self.compute_models.SshPublicKey(path=key['path'], key_data=key['keyData']) + self.compute_models.SshPublicKey(path=key['path'], key_data=key['key_data']) ) # data disk @@ -2116,6 +2275,10 @@ class AzureRMVirtualMachine(AzureRMModuleBase): if data_disk.get('managed_disk'): managed_disk_type = data_disk['managed_disk'].get('storage_account_type') data_disk_managed_disk = self.compute_models.ManagedDiskParameters(storage_account_type=managed_disk_type) + if data_disk.get('disk_encryption_set'): + data_disk_managed_disk.disk_encryption_set = self.compute_models.DiskEncryptionSetParameters( + id=data_disk['disk_encryption_set'] + ) data_disk_vhd = None else: data_disk_vhd = data_disk['vhd']['uri'] @@ -2146,6 +2309,12 @@ class AzureRMVirtualMachine(AzureRMModuleBase): ) vm_resource.security_profile = security_profile + if self.additional_capabilities is not None: + additional_capabilities = self.compute_models.AdditionalCapabilities( + ultra_ssd_enabled=self.additional_capabilities.get('ultra_ssd_enabled') + ) + vm_resource.additional_capabilities = additional_capabilities + self.log("Update virtual machine with parameters:") self.create_or_update_vm(vm_resource, False) @@ -2182,6 +2351,32 @@ class AzureRMVirtualMachine(AzureRMModuleBase): return self.results + def get_os_disk(self, os_disk): + resource_group_name = None + os_disk_name = None + if os_disk.get('os_disk_id') is not None: + os_disk_dict = self.parse_resource_to_dict(os_disk.get('os_disk_id')) + os_disk_name = os_disk_dict.get('name') + resource_group_name = os_disk_dict.get('resource_group') + elif os_disk.get('os_disk_resource_group') is not None and os_disk.get('os_disk_name') is not None: + os_disk_name = os_disk.get('os_disk_name') + resource_group_name = os_disk.get('os_disk_resource_group') + elif os_disk.get('os_disk_name') is not None: + os_disk_name = os_disk.get('name') + resource_group_name = self.resource_group + else: + self.fail("The swap_os_disk must contain one of 'os_disk_name' and 'os_disk_id'") + + try: + os_disk = {} + response = self.compute_client.disks.get(resource_group_name, os_disk_name) + os_disk['name'] = response.name + os_disk['managed_disk'] = dict(id=response.id) + os_disk['create_option'] = response.creation_data.create_option + return os_disk + except Exception as ec: + self.fail('Could not find os disk {0} in resource group {1}'.format(os_disk_name, resource_group_name)) + def get_vm(self): ''' Get the VM with expanded instanceView @@ -2189,7 +2384,23 @@ class AzureRMVirtualMachine(AzureRMModuleBase): :return: VirtualMachine object ''' try: + retry_count = 0 vm = self.compute_client.virtual_machines.get(self.resource_group, self.name, expand='instanceview') + while True: + if retry_count == 20: + self.fail("Error {0} has a provisioning state of Updating. Expecting state to be Successed.".format(self.name)) + + if vm.provisioning_state != 'Succeeded': + retry_count = retry_count + 1 + time.sleep(150) + vm = self.compute_client.virtual_machines.get(self.resource_group, self.name, expand='instanceview') + else: + p_state = None + for s in vm.instance_view.statuses: + if s.code.startswith('PowerState'): + p_state = s.code + if p_state is not None: + break return vm except Exception as exc: self.fail("Error getting virtual machine {0} - {1}".format(self.name, str(exc))) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachine_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachine_info.py index d94f9848e..3df8e2fbc 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachine_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachine_info.py @@ -70,6 +70,12 @@ vms: returned: always type: complex contains: + additional_capabilities: + description: + - Enables or disables a capability on the virtual machine. + type: dict + returned: always + sample: {ultra_ssd_enabled: False} admin_username: description: - Administrator user name. @@ -244,6 +250,12 @@ vms: returned: always type: dict sample: { "key1":"value1" } + vm_agent_version: + description: + - Version of the Azure VM Agent (waagent) running inside the VM. + returned: always + type: str + sample: '2.9.1.1' vm_size: description: - Virtual machine size. @@ -459,6 +471,11 @@ class AzureRMVirtualMachineInfo(AzureRMModuleBase): new_result = {} + if instance.get('vm_agent') is not None: + new_result['vm_agent_version'] = instance['vm_agent'].get('vm_agent_version') + else: + new_result['vm_agent_version'] = 'Unknown' + if vm.security_profile is not None: new_result['security_profile'] = dict() if vm.security_profile.encryption_at_host is not None: @@ -483,6 +500,7 @@ class AzureRMVirtualMachineInfo(AzureRMModuleBase): new_result['vm_size'] = result['hardware_profile']['vm_size'] new_result['proximityPlacementGroup'] = result.get('proximity_placement_group') new_result['zones'] = result.get('zones', None) + new_result['additional_capabilities'] = result.get('additional_capabilities') os_profile = result.get('os_profile') if os_profile is not None: new_result['admin_username'] = os_profile.get('admin_username') diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachinescaleset.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachinescaleset.py index 4a64628fb..c4211a971 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachinescaleset.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachinescaleset.py @@ -334,6 +334,7 @@ options: choices: - Flexible - Uniform + default: Flexible security_profile: description: - Specifies the Security related profile settings for the virtual machine sclaset. @@ -646,6 +647,7 @@ azure_vmss: ''' # NOQA import base64 +import time try: from azure.core.exceptions import ResourceNotFoundError @@ -723,7 +725,9 @@ class AzureRMVirtualMachineScaleSet(AzureRMModuleBase): scale_in_policy=dict(type='str', choices=['Default', 'OldestVM', 'NewestVM']), terminate_event_timeout_minutes=dict(type='int'), ephemeral_os_disk=dict(type='bool'), - orchestration_mode=dict(type='str', choices=['Uniform', 'Flexible']), + orchestration_mode=dict(type='str', + choices=['Uniform', 'Flexible'], + default='Flexible',), platform_fault_domain_count=dict(type='int', default=1), os_disk_size_gb=dict(type='int'), security_profile=dict( @@ -1375,7 +1379,18 @@ class AzureRMVirtualMachineScaleSet(AzureRMModuleBase): :return: VirtualMachineScaleSet object ''' try: + retry_count = 0 vmss = self.compute_client.virtual_machine_scale_sets.get(self.resource_group, self.name) + while True: + if retry_count == 20: + self.fail("Error {0} has a provisioning state of Updating. Expecting state to be Successed.".format(self.name)) + + if vmss.provisioning_state != 'Succeeded': + retry_count = retry_count + 1 + time.sleep(150) + vmss = self.compute_client.virtual_machine_scale_sets.get(self.resource_group, self.name) + else: + break return vmss except ResourceNotFoundError as exc: self.fail("Error getting virtual machine scale set {0} - {1}".format(self.name, str(exc))) diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachinescalesetinstance.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachinescalesetinstance.py index ca20000a8..402af0072 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachinescalesetinstance.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachinescalesetinstance.py @@ -165,7 +165,6 @@ class AzureRMVirtualMachineScaleSetInstance(AzureRMModuleBase): setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(ComputeManagementClient, base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True, api_version='2021-04-01') instances = self.get() diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachinescalesetinstance_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachinescalesetinstance_info.py index 07b6cf92c..47a3d3318 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachinescalesetinstance_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualmachinescalesetinstance_info.py @@ -169,7 +169,6 @@ class AzureRMVirtualMachineScaleSetVMInfo(AzureRMModuleBase): setattr(self, key, kwargs[key]) self.mgmt_client = self.get_mgmt_svc_client(ComputeManagementClient, base_url=self._cloud_environment.endpoints.resource_manager, - is_track2=True, api_version='2021-04-01') if (self.instance_id is None): diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualwan.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualwan.py index 38c542695..53bef71eb 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualwan.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_virtualwan.py @@ -229,7 +229,7 @@ except ImportError: class Actions: - NoAction, Create, Update, Delete = range(4) + NoAction, Create, Update, Update_tags, Delete = range(5) class AzureRMVirtualWan(AzureRMModuleBaseExt): @@ -251,45 +251,34 @@ class AzureRMVirtualWan(AzureRMModuleBaseExt): required=True ), disable_vpn_encryption=dict( - type='bool', - disposition='/disable_vpn_encryption' + type='bool' ), virtual_hubs=dict( type='list', elements='dict', - updatable=False, - disposition='/virtual_hubs', options=dict( id=dict( - type='str', - disposition='id' + type='str' ) ) ), vpn_sites=dict( type='list', elements='dict', - updatable=False, - disposition='/vpn_sites', options=dict( id=dict( - type='str', - disposition='id' + type='str' ) ) ), allow_branch_to_branch_traffic=dict( - type='bool', - disposition='/allow_branch_to_branch_traffic' + type='bool' ), allow_vnet_to_vnet_traffic=dict( - type='bool', - updatable=False, - disposition='/allow_vnet_to_vnet_traffic' + type='bool' ), virtual_wan_type=dict( type='str', - disposition='/virtual_wan_type', choices=['Basic', 'Standard'] ), state=dict( @@ -313,14 +302,12 @@ class AzureRMVirtualWan(AzureRMModuleBaseExt): supports_tags=True) def exec_module(self, **kwargs): - for key in list(self.module_arg_spec.keys()): + for key in list(self.module_arg_spec.keys()) + ['tags']: if hasattr(self, key): setattr(self, key, kwargs[key]) elif kwargs[key] is not None: self.body[key] = kwargs[key] - self.inflate_parameters(self.module_arg_spec, self.body, 0) - resource_group = self.get_resource_group(self.resource_group) if self.location is None: # Set default location @@ -339,18 +326,28 @@ class AzureRMVirtualWan(AzureRMModuleBaseExt): if self.state == 'absent': self.to_do = Actions.Delete else: - modifiers = {} - self.create_compare_modifiers(self.module_arg_spec, '', modifiers) - self.results['modifiers'] = modifiers - self.results['compare'] = [] - if not self.default_compare(modifiers, self.body, old_response, '', self.results): - self.to_do = Actions.Update + compare_list = ['disable_vpn_encryption', 'allow_branch_to_branch_traffic'] + for key in compare_list: + if self.body.get(key) is not None and self.body[key] != old_response[key]: + self.log('parameter {0} does not match the configuration'.format(key)) + self.to_do = Actions.Update + else: + self.body[key] = old_response[key] + + update_tags, self.tags = self.update_tags(old_response.get('tags')) + if update_tags: + self.to_do = Actions.Update_tags if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): self.results['changed'] = True if self.check_mode: return self.results response = self.create_update_resource() + elif self.to_do == Actions.Update_tags: + self.results['changed'] = True + if self.check_mode: + return self.results + response = self.update_resource_tags(dict(tags=self.tags)) elif self.to_do == Actions.Delete: self.results['changed'] = True if self.check_mode: @@ -363,6 +360,18 @@ class AzureRMVirtualWan(AzureRMModuleBaseExt): self.results['state'] = response return self.results + def update_resource_tags(self, tags_parameters): + try: + response = self.network_client.virtual_wans.update_tags(resource_group_name=self.resource_group, + virtual_wan_name=self.name, + wan_parameters=tags_parameters) + if isinstance(response, LROPoller): + response = self.get_poller_result(response) + except Exception as exc: + self.log('Error attempting to Update the VirtualWan instance.') + self.fail('Error Updating the VirtualWan instance tags: {0}'.format(str(exc))) + return response.as_dict() + def create_update_resource(self): try: response = self.network_client.virtual_wans.begin_create_or_update(resource_group_name=self.resource_group, diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_vmbackuppolicy.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_vmbackuppolicy.py index 5cd10c978..d3584b670 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_vmbackuppolicy.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_vmbackuppolicy.py @@ -366,7 +366,6 @@ class VMBackupPolicy(AzureRMModuleBaseExt): response = None self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) old_response = self.get_resource() @@ -403,10 +402,12 @@ class VMBackupPolicy(AzureRMModuleBaseExt): self.log('Error in creating Backup Policy.') self.fail('Error in creating Backup Policy {0}'.format(str(e))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_vmbackuppolicy_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_vmbackuppolicy_info.py index 0aa7b5918..0efe1df6c 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_vmbackuppolicy_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_vmbackuppolicy_info.py @@ -216,7 +216,6 @@ class BackupPolicyVMInfo(AzureRMModuleBaseExt): self.url = self.get_url() self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, - is_track2=True, base_url=self._cloud_environment.endpoints.resource_manager) response = self.get_resource() @@ -243,10 +242,12 @@ class BackupPolicyVMInfo(AzureRMModuleBaseExt): except Exception as e: self.log('Backup policy does not exist.') self.fail('Error in fetching VM Backup Policy {0}'.format(str(e))) - try: + if hasattr(response, 'body'): response = json.loads(response.body()) - except Exception: - response = {'text': response.context['deserialized_data']} + elif hasattr(response, 'context'): + response = response.context['deserialized_data'] + else: + self.fail("Create or Updating fail, no match message return, return info as {0}".format(response)) return response diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_vpnsite.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_vpnsite.py index 32d4fafee..c1b7ff8f8 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_vpnsite.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_vpnsite.py @@ -307,7 +307,7 @@ except ImportError: class Actions: - NoAction, Create, Update, Delete = range(4) + NoAction, Create, Update, Delete, Update_tags = range(5) class AzureRMVpnSite(AzureRMModuleBaseExt): @@ -326,92 +326,71 @@ class AzureRMVpnSite(AzureRMModuleBaseExt): ), virtual_wan=dict( type='dict', - disposition='/virtual_wan', options=dict( id=dict( type='str', - disposition='id' ) ) ), device_properties=dict( type='dict', - disposition='/device_properties', options=dict( device_vendor=dict( type='str', - disposition='device_vendor' ), device_model=dict( type='str', - disposition='device_model' ), link_speed_in_mbps=dict( type='int', - disposition='link_speed_in_mbps' ) ) ), ip_address=dict( type='str', - disposition='/ip_address' ), site_key=dict( type='str', no_log=True, - disposition='/site_key' ), address_space=dict( type='dict', - disposition='/address_space', options=dict( address_prefixes=dict( type='list', - disposition='address_prefixes', elements='str' ) ) ), bgp_properties=dict( type='dict', - disposition='/bgp_properties', options=dict( asn=dict( type='int', - disposition='asn' ), bgp_peering_address=dict( type='str', - disposition='bgp_peering_address' ), peer_weight=dict( type='int', - disposition='peer_weight' ), bgp_peering_addresses=dict( type='list', - disposition='bgp_peering_addresses', elements='dict', options=dict( ipconfiguration_id=dict( type='str', - disposition='ipconfiguration_id' ), default_bgp_ip_addresses=dict( type='list', - updatable=False, - disposition='default_bgp_ip_addresses', elements='str' ), custom_bgp_ip_addresses=dict( type='list', - disposition='custom_bgp_ip_addresses', elements='str' ), tunnel_ip_addresses=dict( type='list', - updatable=False, - disposition='tunnel_ip_addresses', elements='str' ) ) @@ -420,50 +399,39 @@ class AzureRMVpnSite(AzureRMModuleBaseExt): ), is_security_site=dict( type='bool', - disposition='/is_security_site' ), vpn_site_links=dict( type='list', - disposition='/vpn_site_links', elements='dict', options=dict( name=dict( type='str', - disposition='name' ), link_properties=dict( type='dict', - disposition='link_properties', options=dict( link_provider_name=dict( type='str', - disposition='link_provider_name' ), link_speed_in_mbps=dict( type='int', - disposition='link_speed_in_mbps' ) ) ), ip_address=dict( type='str', - disposition='ip_address' ), fqdn=dict( type='str', - disposition='fqdn' ), bgp_properties=dict( type='dict', - disposition='bgp_properties', options=dict( asn=dict( type='int', - disposition='asn' ), bgp_peering_address=dict( type='str', - disposition='bgp_peering_address' ) ) ) @@ -471,23 +439,18 @@ class AzureRMVpnSite(AzureRMModuleBaseExt): ), o365_policy=dict( type='dict', - disposition='/o365_policy', options=dict( break_out_categories=dict( type='dict', - disposition='break_out_categories', options=dict( allow=dict( type='bool', - disposition='allow' ), optimize=dict( type='bool', - disposition='optimize' ), default=dict( type='bool', - disposition='default' ) ) ) @@ -514,7 +477,7 @@ class AzureRMVpnSite(AzureRMModuleBaseExt): supports_tags=True) def exec_module(self, **kwargs): - for key in list(self.module_arg_spec.keys()): + for key in list(self.module_arg_spec.keys()) + ['tags']: if hasattr(self, key): setattr(self, key, kwargs[key]) elif kwargs[key] is not None: @@ -540,18 +503,62 @@ class AzureRMVpnSite(AzureRMModuleBaseExt): if self.state == 'absent': self.to_do = Actions.Delete else: - modifiers = {} - self.create_compare_modifiers(self.module_arg_spec, '', modifiers) - self.results['modifiers'] = modifiers - self.results['compare'] = [] - if not self.default_compare(modifiers, self.body, old_response, '', self.results): + if self.body.get('virtual_wan') is not None and self.body['virtual_wan'] != old_response.get('virtual_wan'): self.to_do = Actions.Update + for key in self.body.keys(): + if key == 'address_space': + if old_response.get('address_space') is None or\ + len(self.body['address_space']['address_prefixes']) > len(old_response['address_space']['address_prefixes']) or\ + not all(key in old_response['address_space']['address_prefixes'] for key in self.body['address_space']['address_prefixes']): + self.to_do = Actions.Update + elif key == 'device_properties': + if old_response.get('device_properties') is None or\ + not all(self.body['device_properties'][key] == old_response['device_properties'].get(key) + for key in self.body['device_properties'].keys()): + self.to_do = Actions.Update + elif key == 'o365_policy': + if old_response.get('o365_policy') is None or\ + not all(self.body['o365_policy']['break_out_categories'][key] == old_response['o365_policy']['break_out_categories'].get(key) + for key in self.body['o365_policy']['break_out_categories'].keys()): + self.to_do = Actions.Update + elif key == 'vpn_site_links': + if old_response.get('vpn_site_links') is None or\ + not all(self.body['vpn_site_links'][key] == old_response['vpn_site_links'].get(key) + for key in self.body['vpn_site_links'].keys()): + self.to_do = Actions.Update + elif key == 'bgp_properties': + if old_response.get('bgp_properties') is None: + self.to_do = Actions.Update + else: + for item in self.body['bgp_properties'].keys(): + if item != 'bgp_peering_addresses' and item != 'peer_weight': + if self.body['bgp_properties'][item] != old_response['bgp_properties'].get(item): + self.to_do = Actions.Update + else: + if self.body['bgp_properties'].get('bgp_peering_addresses') is not None: + bgp_address = old_response['bgp_properties']['bgp_peering_addresses'] + if old_response['bgp_properties'].get('bgp_peering_addresses') is None or\ + not all(self.body['bgp_properties']['bgp_peering_addresses'][value] == bgp_address.get(value) + for value in ['ipconfiguration_id', 'custom_bgp_ip_addresses']): + self.to_do = Actions.Update + + elif self.body[key] != old_response.get(key): + self.to_do = Actions.Update + + update_tags, self.tags = self.update_tags(old_response.get('tags')) + if update_tags: + self.to_do = Actions.Update_tags if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): self.results['changed'] = True if self.check_mode: return self.results response = self.create_update_resource() + elif self.to_do == Actions.Update_tags: + self.results['changed'] = True + if self.check_mode: + return self.results + response = self.update_resource_tags(dict(tags=self.tags)) elif self.to_do == Actions.Delete: self.results['changed'] = True if self.check_mode: @@ -565,6 +572,18 @@ class AzureRMVpnSite(AzureRMModuleBaseExt): self.results['state'] = response return self.results + def update_resource_tags(self, tags_parameters): + try: + response = self.network_client.vpn_sites.update_tags(resource_group_name=self.resource_group, + vpn_site_name=self.name, + vpn_site_parameters=tags_parameters) + if isinstance(response, LROPoller): + response = self.get_poller_result(response) + except Exception as exc: + self.log('Error attempting to update the VpnSite instance tags.') + self.fail('Error updating the VpnSite instance: {0}'.format(str(exc))) + return response.as_dict() + def create_update_resource(self): try: response = self.network_client.vpn_sites.begin_create_or_update(resource_group_name=self.resource_group, diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_webapp.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_webapp.py index 05697176b..e58cbcd43 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_webapp.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_webapp.py @@ -76,13 +76,13 @@ options: version: description: - Version of the framework. For Linux web app supported value, see U(https://aka.ms/linux-stacks) for more info. - - C(net_framework) supported value sample, C(v4.0) for .NET 4.6 and C(v3.0) for .NET 3.5. - - C(php) supported value sample, C(5.5), C(5.6), C(7.0). - - C(python) supported value sample, C(2.7), C(3.8), C(3.10). - - C(node) supported value sample, C(6.6), C(6.9). - - C(dotnetcore) supported value sample, C(1.0), C(1.1), C(1.2). + - C(net_framework) supported value sample, C(v4.8) for .NET 4.8 and C(v3.5) for .NET 3.5. + - C(php) supported value sample, C(8.1), C(8.2). + - C(python) supported value sample, C(3.8), C(3.9), C(3.10), C(3.11), C(3.12). + - C(node) supported value sample, C(18), C(20). + - C(dotnetcore) supported value sample, C(8), C(7), C(6). - C(ruby) supported value sample, C(2.3). - - C(java) supported value sample, C(1.9) for Windows web app. C(1.8) for Linux web app. + - C(java) supported value sample, C(21), C(17), C(11) and C(8). type: str required: true settings: @@ -93,14 +93,14 @@ options: java_container: description: - Name of Java container. - - Supported only when I(frameworks=java). Sample values C(Tomcat), C(Jetty). + - Supported only when I(frameworks=java). Sample values C(Tomcat), C(JavaSE), C(RedHat). type: str required: True java_container_version: description: - Version of Java container. - Supported only when I(frameworks=java). - - Sample values for C(Tomcat), C(8.0), C(8.5), C(9.0). For C(Jetty,), C(9.1), C(9.3). + - Sample values for C(Tomcat), C(8.5), C(9.0), C(10.0), C(10.1). type: str required: True @@ -139,6 +139,11 @@ options: - Keeps the app loaded even when there's no traffic. type: bool + http20_enabled: + description: + - Configures a web site to allow clients to connect over HTTP 2.0. + type: bool + min_tls_version: description: - The minimum TLS encryption version required for the app. @@ -289,7 +294,7 @@ EXAMPLES = ''' testkey: testvalue frameworks: - name: "node" - version: "6.6" + version: "18" - name: Create a windows web app with node, php azure_rm_webapp: @@ -302,9 +307,9 @@ EXAMPLES = ''' testkey: testvalue frameworks: - name: "node" - version: 6.6 + version: 18 - name: "php" - version: "7.0" + version: 8.2 - name: Create a stage deployment slot for an existing web app azure_rm_webapp: @@ -494,6 +499,9 @@ class AzureRMWebApps(AzureRMModuleBase): always_on=dict( type='bool', ), + http20_enabled=dict( + type='bool', + ), min_tls_version=dict( type='str', choices=['1.0', '1.1', '1.2'], @@ -583,6 +591,7 @@ class AzureRMWebApps(AzureRMModuleBase): "python_version", "scm_type", "always_on", + "http20_enabled", "min_tls_version", "ftps_state"] @@ -591,7 +600,7 @@ class AzureRMWebApps(AzureRMModuleBase): "https_only"] self.supported_linux_frameworks = ['ruby', 'php', 'python', 'dotnetcore', 'node', 'java'] - self.supported_windows_frameworks = ['net_framework', 'php', 'python', 'node', 'java'] + self.supported_windows_frameworks = ['net_framework', 'php', 'python', 'node', 'java', 'dotnetcore'] super(AzureRMWebApps, self).__init__(derived_arg_spec=self.module_arg_spec, mutually_exclusive=mutually_exclusive, @@ -605,7 +614,7 @@ class AzureRMWebApps(AzureRMModuleBase): if hasattr(self, key): setattr(self, key, kwargs[key]) elif kwargs[key] is not None: - if key in ["scm_type", "always_on", "min_tls_version", "ftps_state"]: + if key in ["scm_type", "always_on", "http20_enabled", "min_tls_version", "ftps_state"]: self.site_config[key] = kwargs[key] old_response = None @@ -655,15 +664,15 @@ class AzureRMWebApps(AzureRMModuleBase): self.site_config['linux_fx_version'] = (self.frameworks[0]['name'] + '|' + self.frameworks[0]['version']).upper() if self.frameworks[0]['name'] == 'java': - if self.frameworks[0]['version'] != '8': - self.fail("Linux web app only supports java 8.") + if self.frameworks[0]['version'] not in ['8', '11', '17', '21']: + self.fail("Linux web app only supports java 8, 11, 17 and 21.") if self.frameworks[0]['settings'] and self.frameworks[0]['settings']['java_container'].lower() != 'tomcat': self.fail("Linux web app only supports tomcat container.") if self.frameworks[0]['settings'] and self.frameworks[0]['settings']['java_container'].lower() == 'tomcat': self.site_config['linux_fx_version'] = 'TOMCAT|' + self.frameworks[0]['settings']['java_container_version'] + '-jre8' else: - self.site_config['linux_fx_version'] = 'JAVA|8-jre8' + self.site_config['linux_fx_version'] = 'JAVA|{0}-jre{0}'.format(self.frameworks[0]['version']) else: for fx in self.frameworks: if fx.get('name') not in self.supported_windows_frameworks: @@ -852,7 +861,7 @@ class AzureRMWebApps(AzureRMModuleBase): # compare xxx_version def is_site_config_changed(self, existing_config): for updatable_property in self.site_config_updatable_properties: - if self.site_config.get(updatable_property): + if updatable_property in self.site_config: if not getattr(existing_config, updatable_property) or \ str(getattr(existing_config, updatable_property)).upper() != str(self.site_config.get(updatable_property)).upper(): return True diff --git a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_webapp_info.py b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_webapp_info.py index af035b152..c0ec6b42d 100644 --- a/ansible_collections/azure/azcollection/plugins/modules/azure_rm_webapp_info.py +++ b/ansible_collections/azure/azcollection/plugins/modules/azure_rm_webapp_info.py @@ -138,6 +138,12 @@ webapps: returned: always type: bool sample: true + http20_enabled: + description: + - Configures a web site to allow clients to connect over HTTP 2.0. + returned: always + type: bool + sample: true min_tls_version: description: - The minimum TLS encryption version required for the app. @@ -486,6 +492,7 @@ class AzureRMWebAppInfo(AzureRMModuleBase): curated_output['frameworks'].append({'name': tmp[0].lower(), 'version': tmp[1]}) curated_output['always_on'] = configuration.get('always_on') + curated_output['http20_enabled'] = configuration.get('http20_enabled') curated_output['ftps_state'] = configuration.get('ftps_state') curated_output['min_tls_version'] = configuration.get('min_tls_version') |