1#!/usr/local/bin/python3.8 2 3# (c) 2021, NetApp, Inc 4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 6''' 7na_cloudmanager_connector_azure 8''' 9 10from __future__ import absolute_import, division, print_function 11__metaclass__ = type 12 13 14DOCUMENTATION = ''' 15 16module: na_cloudmanager_connector_azure 17short_description: NetApp Cloud Manager connector for Azure. 18extends_documentation_fragment: 19 - netapp.cloudmanager.netapp.cloudmanager 20version_added: '21.4.0' 21author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com> 22 23description: 24- Create or delete Cloud Manager connector for Azure. 25 26options: 27 28 state: 29 description: 30 - Whether the specified Cloud Manager connector for Azure should exist or not. 31 choices: ['present', 'absent'] 32 default: 'present' 33 type: str 34 35 name: 36 required: true 37 description: 38 - The name of the Cloud Manager connector for Azure to manage. 39 type: str 40 41 virtual_machine_size: 42 description: 43 - The virtual machine type. (for example, Standard_DS3_v2). 44 - At least 4 CPU and 16 GB of memory are required. 45 type: str 46 default: Standard_DS3_v2 47 48 resource_group: 49 required: true 50 description: 51 - The resource group in Azure where the resources will be created. 52 type: str 53 54 subnet_name: 55 required: true 56 description: 57 - The name of the subnet for the virtual machine. 58 - For example, in /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/virtualNetworks/xxx/subnets/default, 59 only default is needed. 60 aliases: 61 - subnet_id 62 type: str 63 version_added: '21.7.0' 64 65 location: 66 required: true 67 description: 68 - The location where the Cloud Manager Connector will be created. 69 type: str 70 71 client_id: 72 description: 73 - The unique client ID of the Connector. 74 type: str 75 76 subscription_id: 77 required: true 78 description: 79 - The ID of the Azure subscription. 80 type: str 81 82 company: 83 required: true 84 description: 85 - The name of the company of the user. 86 type: str 87 88 vnet_name: 89 required: true 90 description: 91 - The name of the virtual network. 92 - for example, in /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/virtualNetworks/default, 93 only default is needed. 94 aliases: 95 - vnet_id 96 type: str 97 version_added: '21.7.0' 98 99 vnet_resource_group: 100 description: 101 - The resource group in Azure associated with the virtual network. 102 - If not provided, its assumed that the VNet is within the previously specified resource group. 103 type: str 104 105 network_security_resource_group: 106 description: 107 - The resource group in Azure associated with the security group. 108 - If not provided, its assumed that the security group is within the previously specified resource group. 109 type: str 110 111 network_security_group_name: 112 required: true 113 description: 114 - The name of the security group for the deployment. 115 type: str 116 117 proxy_certificates: 118 description: 119 - The proxy certificates, a list of certificate file names. 120 type: list 121 elements: str 122 123 associate_public_ip_address: 124 description: 125 - Indicates whether to associate the public IP address to the virtual machine. 126 type: bool 127 default: true 128 129 account_id: 130 required: true 131 description: 132 - The NetApp tenancy account ID. 133 type: str 134 135 proxy_url: 136 description: 137 - The proxy URL, if using a proxy to connect to the internet. 138 type: str 139 140 proxy_user_name: 141 description: 142 - The proxy user name, if using a proxy to connect to the internet. 143 type: str 144 145 proxy_password: 146 description: 147 - The proxy password, if using a proxy to connect to the internet. 148 type: str 149 150 admin_username: 151 required: true 152 description: 153 - The user name for the Connector. 154 type: str 155 156 admin_password: 157 required: true 158 description: 159 - The password for the Connector. 160 type: str 161 162''' 163 164EXAMPLES = """ 165- name: Create NetApp Cloud Manager connector for Azure. 166 netapp.cloudmanager.na_cloudmanager_connector_azure: 167 state: present 168 refresh_token: "{{ xxxxxxxxxxxxxxx }}" 169 name: bsuhas_ansible_occm 170 location: westus 171 resource_group: occm_group_westus 172 subnet_name: subnetxxxxx 173 vnet_name: Vnetxxxxx 174 subscription_id: "{{ xxxxxxxxxxxxxxxxx }}" 175 account_id: "{{ account-xxxxxxx }}" 176 company: NetApp 177 admin_password: Netapp123456 178 admin_username: bsuhas 179 network_security_group_name: OCCM_SG 180 proxy_url: abc.com 181 proxy_user_name: xyz 182 proxy_password: abcxyz 183 proxy_certificates: [abc.crt.txt, xyz.crt.txt] 184 185- name: Delete NetApp Cloud Manager connector for Azure. 186 netapp.cloudmanager.na_cloudmanager_connector_azure: 187 state: absent 188 name: ansible 189 location: westus 190 resource_group: occm_group_westus 191 network_security_group_name: OCCM_SG 192 subnet_name: subnetxxxxx 193 company: NetApp 194 admin_password: Netapp123456 195 admin_username: bsuhas 196 vnet_name: Vnetxxxxx 197 subscription_id: "{{ xxxxxxxxxxxxxxxxx }}" 198 account_id: "{{ account-xxxxxxx }}" 199 refresh_token: "{{ xxxxxxxxxxxxxxx }}" 200 client_id: xxxxxxxxxxxxxxxxxxx 201""" 202 203RETURN = """ 204msg: 205 description: Newly created Azure connector id in cloud manager. 206 type: str 207 returned: success 208 sample: 'xxxxxxxxxxxxxxxx' 209""" 210 211import traceback 212import time 213import base64 214import json 215 216from ansible.module_utils.basic import AnsibleModule 217from ansible.module_utils._text import to_native 218import ansible_collections.netapp.cloudmanager.plugins.module_utils.netapp as netapp_utils 219from ansible_collections.netapp.cloudmanager.plugins.module_utils.netapp_module import NetAppModule 220from ansible_collections.netapp.cloudmanager.plugins.module_utils.netapp import CloudManagerRestAPI 221 222IMPORT_EXCEPTION = None 223 224try: 225 from azure.mgmt.resource import ResourceManagementClient 226 from azure.mgmt.compute import ComputeManagementClient 227 from azure.mgmt.network import NetworkManagementClient 228 from azure.mgmt.storage import StorageManagementClient 229 from azure.mgmt.resource.resources.models import Deployment 230 from azure.mgmt.resource.resources.models import DeploymentProperties 231 from azure.common.client_factory import get_client_from_cli_profile 232 from msrestazure.azure_exceptions import CloudError 233 HAS_AZURE_LIB = True 234except ImportError as exc: 235 HAS_AZURE_LIB = False 236 IMPORT_EXCEPTION = exc 237 238 239class NetAppCloudManagerConnectorAzure(object): 240 ''' object initialize and class methods ''' 241 242 def __init__(self): 243 self.use_rest = False 244 self.argument_spec = netapp_utils.cloudmanager_host_argument_spec() 245 self.argument_spec.update(dict( 246 name=dict(required=True, type='str'), 247 state=dict(required=False, choices=['present', 'absent'], default='present'), 248 virtual_machine_size=dict(required=False, type='str', default='Standard_DS3_v2'), 249 resource_group=dict(required=True, type='str'), 250 subscription_id=dict(required=True, type='str'), 251 subnet_name=dict(required=True, type='str', aliases=['subnet_id']), 252 vnet_name=dict(required=True, type='str', aliases=['vnet_id']), 253 vnet_resource_group=dict(required=False, type='str'), 254 location=dict(required=True, type='str'), 255 network_security_resource_group=dict(required=False, type='str'), 256 network_security_group_name=dict(required=True, type='str'), 257 client_id=dict(required=False, type='str'), 258 company=dict(required=True, type='str'), 259 proxy_certificates=dict(required=False, type='list', elements='str'), 260 associate_public_ip_address=dict(required=False, type='bool', default=True), 261 account_id=dict(required=True, type='str'), 262 proxy_url=dict(required=False, type='str', default=''), 263 proxy_user_name=dict(required=False, type='str', default=''), 264 proxy_password=dict(required=False, type='str', default='', no_log=True), 265 admin_username=dict(required=True, type='str'), 266 admin_password=dict(required=True, type='str', no_log=True), 267 )) 268 269 self.module = AnsibleModule( 270 argument_spec=self.argument_spec, 271 required_if=[ 272 ['state', 'absent', ['client_id']] 273 ], 274 required_one_of=[['refresh_token', 'sa_client_id']], 275 required_together=[['sa_client_id', 'sa_secret_key']], 276 supports_check_mode=True 277 ) 278 279 if HAS_AZURE_LIB is False: 280 self.module.fail_json(msg="the python AZURE library azure.mgmt and azure.common is required. Command is pip install azure-mgmt, azure-common." 281 " Import error: %s" % str(IMPORT_EXCEPTION)) 282 283 self.na_helper = NetAppModule() 284 self.parameters = self.na_helper.set_parameters(self.module.params) 285 286 self.rest_api = CloudManagerRestAPI(self.module) 287 288 def get_deploy_azure_vm(self): 289 """ 290 Get Cloud Manager connector for AZURE 291 :return: 292 Dictionary of current details if Cloud Manager connector for AZURE 293 None if Cloud Manager connector for AZURE is not found 294 """ 295 296 exists = False 297 298 resource_client = get_client_from_cli_profile(ResourceManagementClient) 299 300 try: 301 exists = resource_client.deployments.check_existence(self.parameters['resource_group'], self.parameters['name']) 302 303 except CloudError as error: 304 self.module.fail_json(msg=to_native(error), exception=traceback.format_exc()) 305 306 if not exists: 307 return None 308 309 return exists 310 311 def deploy_azure(self): 312 """ 313 Create Cloud Manager connector for Azure 314 :return: client_id 315 """ 316 317 user_data, client_id = self.register_agent_to_service() 318 319 template = json.loads(self.na_helper.call_template()) 320 params = json.loads(self.na_helper.call_parameters()) 321 322 params['adminUsername']['value'] = self.parameters['admin_username'] 323 params['adminPassword']['value'] = self.parameters['admin_password'] 324 params['customData']['value'] = json.dumps(user_data) 325 params['location']['value'] = self.parameters['location'] 326 params['virtualMachineName']['value'] = self.parameters['name'] 327 if self.rest_api.environment == 'stage': 328 params['environment']['value'] = self.rest_api.environment 329 if self.parameters.get('vnet_resource_group') is not None: 330 network = '/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/%s' % ( 331 self.parameters['subscription_id'], self.parameters['vnet_resource_group'], self.parameters['vnet_name']) 332 else: 333 network = '/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/%s' % ( 334 self.parameters['subscription_id'], self.parameters['resource_group'], self.parameters['vnet_name']) 335 336 subnet = '%s/subnets/%s' % (network, self.parameters['subnet_name']) 337 338 if self.parameters.get('network_security_resource_group') is not None: 339 network_security_group_name = '/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/networkSecurityGroups/%s' % ( 340 self.parameters['subscription_id'], self.parameters['network_security_resource_group'], self.parameters['network_security_group_name']) 341 else: 342 network_security_group_name = '/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/networkSecurityGroups/%s' % ( 343 self.parameters['subscription_id'], self.parameters['resource_group'], self.parameters['network_security_group_name']) 344 345 params['virtualNetworkId']['value'] = network 346 params['networkSecurityGroupName']['value'] = network_security_group_name 347 params['virtualMachineSize']['value'] = self.parameters['virtual_machine_size'] 348 params['subnetId']['value'] = subnet 349 350 try: 351 resource_client = get_client_from_cli_profile(ResourceManagementClient) 352 353 resource_client.resource_groups.create_or_update( 354 self.parameters['resource_group'], 355 {"location": self.parameters['location']}) 356 357 deployment_properties = { 358 'mode': 'Incremental', 359 'template': template, 360 'parameters': params 361 } 362 resource_client.deployments.begin_create_or_update( 363 self.parameters['resource_group'], 364 self.parameters['name'], 365 Deployment(properties=deployment_properties) 366 ) 367 368 except CloudError as error: 369 self.module.fail_json(msg="Error in deploy_azure: %s" % to_native(error), exception=traceback.format_exc()) 370 371 # Sleep for 2 minutes 372 time.sleep(120) 373 retries = 30 374 while retries > 0: 375 occm_resp, error = self.na_helper.check_occm_status(self.rest_api.environment_data['CLOUD_MANAGER_HOST'], self.rest_api, client_id) 376 if error is not None: 377 self.module.fail_json( 378 msg="Error: Not able to get occm status: %s, %s" % (str(error), str(occm_resp))) 379 if occm_resp['agent']['status'] == "active": 380 break 381 else: 382 time.sleep(30) 383 retries = retries - 1 384 if retries == 0: 385 # Taking too long for status to be active 386 return self.module.fail_json(msg="Taking too long for OCCM agent to be active or not properly setup") 387 return client_id 388 389 def register_agent_to_service(self): 390 """ 391 Register agent to service and collect userdata by setting up connector 392 :return: UserData, ClientID 393 """ 394 395 if self.parameters.get('vnet_resource_group') is not None: 396 network = '/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/%s' % ( 397 self.parameters['subscription_id'], self.parameters['vnet_resource_group'], self.parameters['vnet_name']) 398 else: 399 network = '/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/%s' % ( 400 self.parameters['subscription_id'], self.parameters['resource_group'], self.parameters['vnet_name']) 401 402 subnet = '%s/subnets/%s' % (network, self.parameters['subnet_name']) 403 404 if self.parameters.get('account_id') is None: 405 response, error = self.na_helper.get_account(self.rest_api.environment_data['CLOUD_MANAGER_HOST'], self.rest_api) 406 if error is not None: 407 self.module.fail_json( 408 msg="Error: unexpected response on getting account: %s, %s" % (str(error), str(response))) 409 self.parameters['account_id'] = response 410 411 headers = { 412 "X-User-Token": self.rest_api.token_type + " " + self.rest_api.token, 413 } 414 body = { 415 "accountId": self.parameters['account_id'], 416 "name": self.parameters['name'], 417 "company": self.parameters['company'], 418 "placement": { 419 "provider": "AZURE", 420 "region": self.parameters['location'], 421 "network": network, 422 "subnet": subnet, 423 }, 424 "extra": { 425 "proxy": { 426 "proxyUrl": self.parameters.get('proxy_url'), 427 "proxyUserName": self.parameters.get('proxy_user_name'), 428 "proxyPassword": self.parameters.get('proxy_password') 429 } 430 } 431 } 432 433 register_url = "%s/agents-mgmt/connector-setup" % self.rest_api.environment_data['CLOUD_MANAGER_HOST'] 434 response, error, dummy = self.rest_api.post(register_url, body, header=headers) 435 if error is not None: 436 self.module.fail_json(msg="Error: unexpected response on getting userdata for connector setup: %s, %s" % (str(error), str(response))) 437 client_id = response['clientId'] 438 439 proxy_certificates = [] 440 if self.parameters.get('proxy_certificates') is not None: 441 for each in self.parameters['proxy_certificates']: 442 try: 443 data = open(each, "r").read() 444 except OSError: 445 self.module.fail_json(msg="Error: Could not open/read file of proxy_certificates: %s" % str(each)) 446 447 encoded_certificate = base64.b64encode(data) 448 proxy_certificates.append(encoded_certificate) 449 450 if len(proxy_certificates) > 0: 451 response['proxySettings']['proxyCertificates'] = proxy_certificates 452 453 return response, client_id 454 455 def delete_azure_occm(self): 456 """ 457 Delete OCCM 458 :return: 459 None 460 """ 461 # delete vm deploy 462 try: 463 compute_client = get_client_from_cli_profile(ComputeManagementClient) 464 vm_delete = compute_client.virtual_machines.begin_delete( 465 self.parameters['resource_group'], 466 self.parameters['name']) 467 while not vm_delete.done(): 468 vm_delete.wait(2) 469 except CloudError as error: 470 self.module.fail_json(msg=to_native(error), exception=traceback.format_exc()) 471 472 # delete interfaces deploy 473 try: 474 network_client = get_client_from_cli_profile(NetworkManagementClient) 475 interface_delete = network_client.network_interfaces.begin_delete( 476 self.parameters['resource_group'], 477 self.parameters['name'] + '-nic') 478 while not interface_delete.done(): 479 interface_delete.wait(2) 480 except CloudError as error: 481 self.module.fail_json(msg=to_native(error), exception=traceback.format_exc()) 482 483 # delete storage account deploy 484 try: 485 storage_client = get_client_from_cli_profile(StorageManagementClient) 486 storage_client.storage_accounts.delete( 487 self.parameters['resource_group'], 488 self.parameters['name'] + 'sa') 489 except CloudError as error: 490 self.module.fail_json(msg=to_native(error), exception=traceback.format_exc()) 491 492 # delete storage account deploy 493 try: 494 network_client = get_client_from_cli_profile(NetworkManagementClient) 495 public_ip_addresses_delete = network_client.public_ip_addresses.begin_delete( 496 self.parameters['resource_group'], 497 self.parameters['name'] + '-ip') 498 while not public_ip_addresses_delete.done(): 499 public_ip_addresses_delete.wait(2) 500 except CloudError as error: 501 self.module.fail_json(msg=to_native(error), exception=traceback.format_exc()) 502 503 # delete deployment 504 try: 505 resource_client = get_client_from_cli_profile(ResourceManagementClient) 506 deployments_delete = resource_client.deployments.begin_delete( 507 self.parameters['resource_group'], 508 self.parameters['name'] + '-ip') 509 while not deployments_delete.done(): 510 deployments_delete.wait(5) 511 except CloudError as error: 512 self.module.fail_json(msg=to_native(error), exception=traceback.format_exc()) 513 514 retries = 16 515 while retries > 0: 516 occm_resp, error = self.na_helper.check_occm_status(self.rest_api.environment_data['CLOUD_MANAGER_HOST'], self.rest_api, 517 self.parameters['client_id']) 518 if error is not None: 519 self.module.fail_json( 520 msg="Error: Not able to get occm status: %s, %s" % (str(error), str(occm_resp))) 521 if occm_resp['agent']['status'] != "active": 522 break 523 else: 524 time.sleep(10) 525 retries = retries - 1 526 if retries == 0: 527 # Taking too long for terminating OCCM 528 return self.module.fail_json(msg="Taking too long for instance to finish terminating") 529 530 client = self.rest_api.format_cliend_id(self.parameters['client_id']) 531 delete_occum_url = "%s/agents-mgmt/agent/%s" % (self.rest_api.environment_data['CLOUD_MANAGER_HOST'], client) 532 headers = { 533 "X-User-Token": self.rest_api.token_type + " " + self.rest_api.token, 534 "X-Tenancy-Account-Id": self.parameters['account_id'] 535 } 536 537 response, error, dummy = self.rest_api.delete(delete_occum_url, None, header=headers) 538 if error is not None: 539 self.module.fail_json(msg="Error: unexpected response on deleting OCCM: %s, %s" % (str(error), str(response))) 540 541 def apply(self): 542 """ 543 Apply action to the Cloud Manager connector for AZURE 544 :return: None 545 """ 546 client_id = None 547 if self.module.check_mode: 548 pass 549 else: 550 if self.parameters['state'] == 'present': 551 client_id = self.deploy_azure() 552 self.na_helper.changed = True 553 elif self.parameters['state'] == 'absent': 554 get_deploy = self.get_deploy_azure_vm() 555 if get_deploy: 556 self.delete_azure_occm() 557 self.na_helper.changed = True 558 559 self.module.exit_json(changed=self.na_helper.changed, msg={'client_id': client_id}) 560 561 562def main(): 563 """ 564 Create Cloud Manager connector for AZURE class instance and invoke apply 565 :return: None 566 """ 567 obj_store = NetAppCloudManagerConnectorAzure() 568 obj_store.apply() 569 570 571if __name__ == '__main__': 572 main() 573