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