1# This code is part of Ansible, but is an independent component.
2# This particular file snippet, and this file snippet only, is BSD licensed.
3# Modules you write using this snippet, which is embedded dynamically by Ansible
4# still belong to the author of the module, and may assign their own license
5# to the complete work.
6#
7# Copyright (c) 2021, Laurent Nicolas <laurentn@netapp.com>
8# All rights reserved.
9#
10# Redistribution and use in source and binary forms, with or without modification,
11# are permitted provided that the following conditions are met:
12#
13#    * Redistributions of source code must retain the above copyright
14#      notice, this list of conditions and the following disclaimer.
15#    * Redistributions in binary form must reproduce the above copyright notice,
16#      this list of conditions and the following disclaimer in the documentation
17#      and/or other materials provided with the distribution.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
27# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29''' Support class for NetApp ansible modules '''
30
31from __future__ import (absolute_import, division, print_function)
32__metaclass__ = type
33
34from copy import deepcopy
35import json
36import re
37import base64
38
39
40def cmp(a, b):
41    '''
42    Python 3 does not have a cmp function, this will do the cmp.
43    :param a: first object to check
44    :param b: second object to check
45    :return:
46    '''
47    # convert to lower case for string comparison.
48    if a is None:
49        return -1
50    if isinstance(a, str) and isinstance(b, str):
51        a = a.lower()
52        b = b.lower()
53    # if list has string element, convert string to lower case.
54    if isinstance(a, list) and isinstance(b, list):
55        a = [x.lower() if isinstance(x, str) else x for x in a]
56        b = [x.lower() if isinstance(x, str) else x for x in b]
57        a.sort()
58        b.sort()
59    return (a > b) - (a < b)
60
61
62class NetAppModule(object):
63    '''
64    Common class for NetApp modules
65    set of support functions to derive actions based
66    on the current state of the system, and a desired state
67    '''
68
69    def __init__(self):
70        self.log = []
71        self.changed = False
72        self.parameters = {'name': 'not intialized'}
73
74    def set_parameters(self, ansible_params):
75        self.parameters = {}
76        for param in ansible_params:
77            if ansible_params[param] is not None:
78                self.parameters[param] = ansible_params[param]
79        return self.parameters
80
81    def get_cd_action(self, current, desired):
82        ''' takes a desired state and a current state, and return an action:
83            create, delete, None
84            eg:
85            is_present = 'absent'
86            some_object = self.get_object(source)
87            if some_object is not None:
88                is_present = 'present'
89            action = cd_action(current=is_present, desired = self.desired.state())
90        '''
91        desired_state = desired['state'] if 'state' in desired else 'present'
92        if current is None and desired_state == 'absent':
93            return None
94        if current is not None and desired_state == 'present':
95            return None
96        # change in state
97        self.changed = True
98        if current is not None:
99            return 'delete'
100        return 'create'
101
102    def compare_and_update_values(self, current, desired, keys_to_compare):
103        updated_values = {}
104        is_changed = False
105        for key in keys_to_compare:
106            if key in current:
107                if key in desired and desired[key] is not None:
108                    if current[key] != desired[key]:
109                        updated_values[key] = desired[key]
110                        is_changed = True
111                    else:
112                        updated_values[key] = current[key]
113                else:
114                    updated_values[key] = current[key]
115
116        return updated_values, is_changed
117
118    def get_working_environments_info(self, rest_api, headers):
119        '''
120        Get all working environments info
121        '''
122        api = "/occm/api/working-environments"
123        response, error, dummy = rest_api.get(api, None, header=headers)
124        if error is not None:
125            return response, error
126        else:
127            return response, None
128
129    def look_up_working_environment_by_name_in_list(self, we_list, name='working_environment_name'):
130        '''
131        Look up working environment by the name in working environment list
132        '''
133        for we in we_list:
134            if we['name'] == name:
135                return we, None
136        return None, "Working environment not found"
137
138    def get_working_environment_details_by_name(self, rest_api, headers, name='working_environment_name', provider=None):
139        '''
140        Use working environment name to get working environment details including:
141        name: working environment name,
142        publicID: working environment ID
143        cloudProviderName,
144        isHA,
145        svmName
146        '''
147        # check the working environment exist or not
148        api = "/occm/api/working-environments/exists/" + name
149        response, error, dummy = rest_api.get(api, None, header=headers)
150        if error is not None:
151            return None, error
152
153        # get working environment lists
154        api = "/occm/api/working-environments"
155        response, error, dummy = rest_api.get(api, None, header=headers)
156        if error is not None:
157            return None, error
158        # look up the working environment in the working environment lists
159        if provider is None or provider == 'onPrem':
160            working_environment_details, error = self.look_up_working_environment_by_name_in_list(response['onPremWorkingEnvironments'], name)
161            if error is None:
162                return working_environment_details, None
163        if provider is None or provider == 'gcp':
164            working_environment_details, error = self.look_up_working_environment_by_name_in_list(response['gcpVsaWorkingEnvironments'], name)
165            if error is None:
166                return working_environment_details, None
167        if provider is None or provider == 'azure':
168            working_environment_details, error = self.look_up_working_environment_by_name_in_list(response['azureVsaWorkingEnvironments'], name)
169            if error is None:
170                return working_environment_details, None
171        if provider is None or provider == 'aws':
172            working_environment_details, error = self.look_up_working_environment_by_name_in_list(response['vsaWorkingEnvironments'], name)
173            if error is None:
174                return working_environment_details, None
175        return None, "Working environment not found"
176
177    def get_working_environment_details(self, rest_api, headers):
178        '''
179        Use working environment id to get working environment details including:
180        name: working environment name,
181        publicID: working environment ID
182        cloudProviderName,
183        isHA,
184        svmName
185        '''
186        api = "/occm/api/working-environments/"
187        api += self.parameters['working_environment_id']
188        response, error, dummy = rest_api.get(api, None, header=headers)
189        if error:
190            return None, error
191        return response, None
192
193    def get_working_environment_detail_for_snapmirror(self, rest_api, headers):
194
195        if self.parameters.get('source_working_environment_id'):
196            api = '/occm/api/working-environments/'
197            api += self.parameters['source_working_environment_id']
198            source_working_env_detail, error, dummy = rest_api.get(api, None, header=headers)
199            if error:
200                return None, None, "Error getting WE info: %s: %s" % (error, source_working_env_detail)
201        elif self.parameters.get('source_working_environment_name'):
202            source_working_env_detail, error = self.get_working_environment_details_by_name(rest_api, headers, 'source_working_environment_name')
203            if error:
204                return None, None, error
205        else:
206            return None, None, "Cannot find working environment by source_working_environment_id or source_working_environment_name"
207
208        if self.parameters.get('destination_working_environment_id'):
209            api = '/occm/api/working-environments/'
210            api += self.parameters['destination_working_environment_id']
211            dest_working_env_detail, error, dummy = rest_api.get(api, None, header=headers)
212            if error:
213                return None, None, "Error getting WE info: %s: %s" % (error, dest_working_env_detail)
214        elif self.parameters.get('destination_working_environment_name'):
215            dest_working_env_detail, error = self.get_working_environment_details_by_name(rest_api, headers, 'destination_working_environment_name')
216            if error:
217                return None, None, error
218        else:
219            return None, None, "Cannot find working environment by destination_working_environment_id or destination_working_environment_name"
220
221        return source_working_env_detail, dest_working_env_detail, None
222
223    def create_account(self, host, rest_api):
224        """
225        Create Account
226        :return: Account ID
227        """
228        headers = {
229            "X-User-Token": rest_api.token_type + " " + rest_api.token,
230        }
231
232        url = host + '/tenancy/account/MyAccount'
233        account_res, error, dummy = rest_api.post(url, header=headers)
234        account_id = None if error is not None else account_res['accountPublicId']
235        return account_id, error
236
237    def get_account(self, host, rest_api):
238        """
239        Get Account
240        :return: Account ID
241        """
242        headers = {
243            "X-User-Token": rest_api.token_type + " " + rest_api.token,
244        }
245
246        url = host + '/tenancy/account'
247        account_res, error, dummy = rest_api.get(url, header=headers)
248        if error is not None:
249            return None, error
250        if len(account_res) == 0:
251            account_id, error = self.create_account(host, rest_api)
252            if error is not None:
253                return None, error
254            return account_id, None
255
256        return account_res[0]['accountPublicId'], None
257
258    def get_accounts_info(self, rest_api, headers):
259        '''
260        Get all accounts info
261        '''
262        api = "/occm/api/accounts"
263        response, error, dummy = rest_api.get(api, None, header=headers)
264        if error is not None:
265            return None, error
266        else:
267            return response, None
268
269    def set_api_root_path(self, working_environment_details, rest_api):
270        '''
271        set API url root path based on the working environment provider
272        '''
273        provider = working_environment_details['cloudProviderName']
274        is_ha = working_environment_details['isHA']
275        api_root_path = None
276        if provider == "Amazon":
277            api_root_path = "/occm/api/aws/ha" if is_ha else "/occm/api/vsa"
278        elif is_ha:
279            api_root_path = "/occm/api/" + provider.lower() + "/ha"
280        else:
281            api_root_path = "/occm/api/" + provider.lower() + "/vsa"
282        rest_api.api_root_path = api_root_path
283
284    def have_required_parameters(self, action):
285        '''
286        Check if all the required parameters in self.params are available or not besides the mandatory parameters
287        '''
288        actions = {'create_aggregate': ['number_of_disks', 'disk_size_size', 'disk_size_unit', 'working_environment_id'],
289                   'update_aggregate': ['number_of_disks', 'disk_size_size', 'disk_size_unit', 'working_environment_id'],
290                   'delete_aggregate': ['working_environment_id'],
291                   }
292        missed_params = [
293            parameter
294            for parameter in actions[action]
295            if parameter not in self.parameters
296        ]
297
298        if not missed_params:
299            return True, None
300        else:
301            return False, missed_params
302
303    def get_modified_attributes(self, current, desired, get_list_diff=False):
304        ''' takes two dicts of attributes and return a dict of attributes that are
305            not in the current state
306            It is expected that all attributes of interest are listed in current and
307            desired.
308            :param: current: current attributes in ONTAP
309            :param: desired: attributes from playbook
310            :param: get_list_diff: specifies whether to have a diff of desired list w.r.t current list for an attribute
311            :return: dict of attributes to be modified
312            :rtype: dict
313
314            NOTE: depending on the attribute, the caller may need to do a modify or a
315            different operation (eg move volume if the modified attribute is an
316            aggregate name)
317        '''
318        # if the object does not exist,  we can't modify it
319        modified = {}
320        if current is None:
321            return modified
322
323        # error out if keys do not match
324        # self.check_keys(current, desired)
325
326        # collect changed attributes
327        for key, value in current.items():
328            if key in desired and desired[key] is not None:
329                if isinstance(value, list):
330                    modified_list = self.compare_lists(value, desired[key], get_list_diff)  # get modified list from current and desired
331                    if modified_list is not None:
332                        modified[key] = modified_list
333                elif isinstance(value, dict):
334                    modified_dict = self.get_modified_attributes(value, desired[key])
335                    if modified_dict:
336                        modified[key] = modified_dict
337                else:
338                    try:
339                        result = cmp(value, desired[key])
340                    except TypeError as exc:
341                        raise TypeError("%s, key: %s, value: %s, desired: %s" % (repr(exc), key, repr(value), repr(desired[key])))
342                    else:
343                        if result != 0:
344                            modified[key] = desired[key]
345        if modified:
346            self.changed = True
347        return modified
348
349    @staticmethod
350    def compare_lists(current, desired, get_list_diff):
351        ''' compares two lists and return a list of elements that are either the desired elements or elements that are
352            modified from the current state depending on the get_list_diff flag
353            :param: current: current item attribute in ONTAP
354            :param: desired: attributes from playbook
355            :param: get_list_diff: specifies whether to have a diff of desired list w.r.t current list for an attribute
356            :return: list of attributes to be modified
357            :rtype: list
358        '''
359        current_copy = deepcopy(current)
360        desired_copy = deepcopy(desired)
361
362        # get what in desired and not in current
363        desired_diff_list = list()
364        for item in desired:
365            if item in current_copy:
366                current_copy.remove(item)
367            else:
368                desired_diff_list.append(item)
369
370        # get what in current but not in desired
371        current_diff_list = []
372        for item in current:
373            if item in desired_copy:
374                desired_copy.remove(item)
375            else:
376                current_diff_list.append(item)
377
378        if desired_diff_list or current_diff_list:
379            # there are changes
380            if get_list_diff:
381                return desired_diff_list
382            else:
383                return desired
384        else:
385            return None
386
387    @staticmethod
388    def convert_module_args_to_api(parameters, exclusion=None):
389        '''
390        Convert a list of string module args to API option format.
391        For example, convert test_option to testOption.
392        :param parameters: dict of parameters to be converted.
393        :param exclusion: list of parameters to be ignored.
394        :return: dict of key value pairs.
395        '''
396        exclude_list = ['api_url', 'token_type', 'refresh_token', 'sa_secret_key', 'sa_client_id']
397        if exclusion is not None:
398            exclude_list += exclusion
399        api_keys = {}
400        for k, v in parameters.items():
401            if k not in exclude_list:
402                words = k.split("_")
403                api_key = ""
404                for word in words:
405                    if len(api_key) > 0:
406                        word = word.title()
407                    api_key += word
408                api_keys[api_key] = v
409        return api_keys
410
411    @staticmethod
412    def convert_data_to_tabbed_jsonstring(data):
413        '''
414        Convert a dictionary data to json format string
415        '''
416        dump = json.dumps(data, indent=2, separators=(',', ': '))
417        return re.sub(
418            '\n +',
419            lambda match: '\n' + '\t' * int(len(match.group().strip('\n')) / 2),
420            dump,
421        )
422
423    @staticmethod
424    def encode_certificates(certificate_file):
425        '''
426        Read certificate file and encode it
427        '''
428        try:
429            fh = open(certificate_file, mode='rb')
430        except (OSError, IOError) as error:
431            return None, error
432        with fh:
433            cert = fh.read()
434            if cert is None:
435                return None, "Error: file is empty"
436            return base64.b64encode(cert).decode('utf-8'), None
437
438    @staticmethod
439    def get_occm_agents(host, rest_api, account_id, name, provider):
440        """
441        Collect a list of agents matching account_id, name, and provider.
442        :return: list of agents, error
443        """
444
445        # I tried to query by name and provider in addition to account_id, but it returned everything
446        params = {'account_id': account_id}
447        get_occum_url = "%s/agents-mgmt/agent" % host
448        headers = {
449            "X-User-Token": rest_api.token_type + " " + rest_api.token,
450        }
451        agents, error, dummy = rest_api.get(get_occum_url, header=headers, params=params)
452        if isinstance(agents, dict) and 'agents' in agents:
453            agents = [agent for agent in agents['agents'] if agent['name'] == name and agent['provider'] == provider]
454        return agents, error
455
456    @staticmethod
457    def get_occm_agent(host, rest_api, client_id):
458        """
459        Fetch OCCM agent given its client id
460        :return: agent details, error
461        """
462        agent, error = NetAppModule.check_occm_status(host, rest_api, client_id)
463        if isinstance(agent, dict) and 'agent' in agent:
464            agent = agent['agent']
465        return agent, error
466
467    @staticmethod
468    def check_occm_status(host, rest_api, client_id):
469        """
470        Check OCCM status
471        :return: status
472        TO BE DEPRECATED - use get_occm_agent
473        """
474
475        get_occm_url = host + "/agents-mgmt/agent/" + rest_api.format_cliend_id(client_id)
476        headers = {
477            "X-User-Token": rest_api.token_type + " " + rest_api.token,
478        }
479        occm_status, error, dummy = rest_api.get(get_occm_url, header=headers)
480        return occm_status, error
481
482    def register_agent_to_service(self, host, rest_api, provider, vpc):
483        '''
484        register agent to service
485        '''
486        api_url = host + '/agents-mgmt/connector-setup'
487
488        headers = {
489            "X-User-Token": rest_api.token_type + " " + rest_api.token,
490        }
491        body = {
492            "accountId": self.parameters['account_id'],
493            "name": self.parameters['name'],
494            "company": self.parameters['company'],
495            "placement": {
496                "provider": provider,
497                "region": self.parameters['region'],
498                "network": vpc,
499                "subnet": self.parameters['subnet_id'],
500            },
501            "extra": {
502                "proxy": {
503                    "proxyUrl": self.parameters.get('proxy_url'),
504                    "proxyUserName": self.parameters.get('proxy_user_name'),
505                    "proxyPassword": self.parameters.get('proxy_password'),
506                }
507            }
508        }
509
510        if provider == "AWS":
511            body['placement']['network'] = vpc
512
513        response, error, dummy = rest_api.post(api_url, body, header=headers)
514        return response, error
515
516    def delete_occm(self, host, rest_api, client_id):
517        '''
518        delete occm
519        '''
520        api_url = host + '/agents-mgmt/agent/' + rest_api.format_cliend_id(client_id)
521        headers = {
522            "X-User-Token": rest_api.token_type + " " + rest_api.token,
523            "X-Tenancy-Account-Id": self.parameters['account_id'],
524        }
525
526        occm_status, error, dummy = rest_api.delete(api_url, None, header=headers)
527        return occm_status, error
528
529    def delete_occm_agents(self, host, rest_api, agents):
530        '''
531        delete a list of occm
532        '''
533        results = []
534        for agent in agents:
535            if 'agentId' in agent:
536                occm_status, error = self.delete_occm(host, rest_api, agent['agentId'])
537            else:
538                occm_status, error = None, 'unexpected agent contents: %s' % repr(agent)
539            if error:
540                results.append((occm_status, error))
541        return results
542
543    @staticmethod
544    def call_parameters():
545        return """
546        {
547            "location": {
548                "value": "string"
549            },
550            "virtualMachineName": {
551                "value": "string"
552            },
553            "virtualMachineSize": {
554                "value": "string"
555            },
556            "networkSecurityGroupName": {
557                "value": "string"
558            },
559            "adminUsername": {
560                "value": "string"
561            },
562            "virtualNetworkId": {
563                "value": "string"
564            },
565            "adminPassword": {
566                "value": "string"
567            },
568            "subnetId": {
569                "value": "string"
570            },
571            "customData": {
572            "value": "string"
573            },
574            "environment": {
575                "value": "prod"
576            }
577        }
578        """
579
580    @staticmethod
581    def call_template():
582        return """
583        {
584        "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
585        "contentVersion": "1.0.0.0",
586        "parameters": {
587            "location": {
588                "type": "string",
589                "defaultValue": "eastus"
590            },
591            "virtualMachineName": {
592                "type": "string"
593            },
594            "virtualMachineSize":{
595                "type": "string"
596            },
597            "adminUsername": {
598                "type": "string"
599            },
600            "virtualNetworkId": {
601                "type": "string"
602            },
603            "networkSecurityGroupName": {
604                "type": "string"
605            },
606            "adminPassword": {
607                "type": "securestring"
608            },
609            "subnetId": {
610                "type": "string"
611            },
612            "customData": {
613              "type": "string"
614            },
615            "environment": {
616              "type": "string",
617              "defaultValue": "prod"
618            }
619        },
620        "variables": {
621            "vnetId": "[parameters('virtualNetworkId')]",
622            "subnetRef": "[parameters('subnetId')]",
623            "networkInterfaceName": "[concat(parameters('virtualMachineName'),'-nic')]",
624            "diagnosticsStorageAccountName": "[concat(toLower(parameters('virtualMachineName')),'sa')]",
625            "diagnosticsStorageAccountId": "[concat('Microsoft.Storage/storageAccounts/', variables('diagnosticsStorageAccountName'))]",
626            "diagnosticsStorageAccountType": "Standard_LRS",
627            "publicIpAddressName": "[concat(parameters('virtualMachineName'),'-ip')]",
628            "publicIpAddressType": "Dynamic",
629            "publicIpAddressSku": "Basic",
630            "msiExtensionName": "ManagedIdentityExtensionForLinux",
631            "occmOffer": "[if(equals(parameters('environment'), 'stage'), 'netapp-oncommand-cloud-manager-staging-preview', 'netapp-oncommand-cloud-manager')]"
632        },
633        "resources": [
634            {
635                "name": "[parameters('virtualMachineName')]",
636                "type": "Microsoft.Compute/virtualMachines",
637                "apiVersion": "2018-04-01",
638                "location": "[parameters('location')]",
639                "dependsOn": [
640                    "[concat('Microsoft.Network/networkInterfaces/', variables('networkInterfaceName'))]",
641                    "[concat('Microsoft.Storage/storageAccounts/', variables('diagnosticsStorageAccountName'))]"
642                ],
643                "properties": {
644                    "osProfile": {
645                        "computerName": "[parameters('virtualMachineName')]",
646                        "adminUsername": "[parameters('adminUsername')]",
647                        "adminPassword": "[parameters('adminPassword')]",
648                        "customData": "[base64(parameters('customData'))]"
649                    },
650                    "hardwareProfile": {
651                        "vmSize": "[parameters('virtualMachineSize')]"
652                    },
653                    "storageProfile": {
654                        "imageReference": {
655                            "publisher": "netapp",
656                            "offer": "[variables('occmOffer')]",
657                            "sku": "occm-byol",
658                            "version": "latest"
659                        },
660                        "osDisk": {
661                            "createOption": "fromImage",
662                            "managedDisk": {
663                                "storageAccountType": "Premium_LRS"
664                            }
665                        },
666                        "dataDisks": []
667                    },
668                    "networkProfile": {
669                        "networkInterfaces": [
670                            {
671                                "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('networkInterfaceName'))]"
672                            }
673                        ]
674                    },
675                    "diagnosticsProfile": {
676                      "bootDiagnostics": {
677                        "enabled": true,
678                        "storageUri":
679                          "[concat('https://', variables('diagnosticsStorageAccountName'), '.blob.core.windows.net/')]"
680                      }
681                    }
682                },
683                "plan": {
684                    "name": "occm-byol",
685                    "publisher": "netapp",
686                    "product": "[variables('occmOffer')]"
687                },
688                "identity": {
689                    "type": "systemAssigned"
690                }
691            },
692            {
693                "apiVersion": "2017-12-01",
694                "type": "Microsoft.Compute/virtualMachines/extensions",
695                "name": "[concat(parameters('virtualMachineName'),'/', variables('msiExtensionName'))]",
696                "location": "[parameters('location')]",
697                "dependsOn": [
698                    "[concat('Microsoft.Compute/virtualMachines/', parameters('virtualMachineName'))]"
699                ],
700                "properties": {
701                    "publisher": "Microsoft.ManagedIdentity",
702                    "type": "[variables('msiExtensionName')]",
703                    "typeHandlerVersion": "1.0",
704                    "autoUpgradeMinorVersion": true,
705                    "settings": {
706                        "port": 50342
707                    }
708                }
709            },
710            {
711                "name": "[variables('diagnosticsStorageAccountName')]",
712                "type": "Microsoft.Storage/storageAccounts",
713                "apiVersion": "2015-06-15",
714                "location": "[parameters('location')]",
715                "properties": {
716                  "accountType": "[variables('diagnosticsStorageAccountType')]"
717                }
718            },
719            {
720                "name": "[variables('networkInterfaceName')]",
721                "type": "Microsoft.Network/networkInterfaces",
722                "apiVersion": "2018-04-01",
723                "location": "[parameters('location')]",
724                "dependsOn": [
725                    "[concat('Microsoft.Network/publicIpAddresses/', variables('publicIpAddressName'))]"
726                ],
727                "properties": {
728                    "ipConfigurations": [
729                        {
730                            "name": "ipconfig1",
731                            "properties": {
732                                "subnet": {
733                                    "id": "[variables('subnetRef')]"
734                                },
735                                "privateIPAllocationMethod": "Dynamic",
736                                "publicIpAddress": {
737                                    "id": "[resourceId(resourceGroup().name,'Microsoft.Network/publicIpAddresses', variables('publicIpAddressName'))]"
738                                }
739                            }
740                        }
741                    ],
742                    "networkSecurityGroup": {
743                        "id": "[parameters('networkSecurityGroupName')]"
744                    }
745                }
746            },
747            {
748                "name": "[variables('publicIpAddressName')]",
749                "type": "Microsoft.Network/publicIpAddresses",
750                "apiVersion": "2017-08-01",
751                "location": "[parameters('location')]",
752                "properties": {
753                    "publicIpAllocationMethod": "[variables('publicIpAddressType')]"
754                },
755                "sku": {
756                    "name": "[variables('publicIpAddressSku')]"
757                }
758            }
759        ],
760        "outputs": {
761            "publicIpAddressName": {
762                "type": "string",
763                "value": "[variables('publicIpAddressName')]"
764            }
765        }
766    }
767    """
768
769    def get_tenant(self, rest_api, headers):
770        """
771        Get workspace ID (tenant)
772        """
773        api_url = '/occm/api/tenants'
774        response, error, dummy = rest_api.get(api_url, header=headers)
775        if error is not None:
776            return None, 'Error: unexpected response on getting tenant for cvo: %s, %s' % (str(error), str(response))
777
778        return response[0]['publicId'], None
779
780    def get_nss(self, rest_api, headers):
781        """
782        Get nss account
783        """
784        api_url = '/occm/api/accounts'
785        response, error, dummy = rest_api.get(api_url, header=headers)
786        if error is not None:
787            return None, 'Error: unexpected response on getting nss for cvo: %s, %s' % (str(error), str(response))
788
789        if len(response['nssAccounts']) == 0:
790            return None, "Error: could not find any NSS account"
791
792        return response['nssAccounts'][0]['publicId'], None
793
794    def get_working_environment_property(self, rest_api, headers):
795        # GET /vsa/working-environments/{workingEnvironmentId}?fields=status,awsProperties,ontapClusterProperties
796        api = '%s/working-environments/%s' % (rest_api.api_root_path, self.parameters['working_environment_id'])
797        api += '?fields=status,providerProperties,ontapClusterProperties'
798        response, error, dummy = rest_api.get(api, None, header=headers)
799        if error:
800            return None, error
801        return response, None
802
803    def compare_cvo_tags_labels(self, current_tags, user_tags, tag_name):
804        '''
805        Compare exiting tags/labels and user input tags/labels to see if there is a change
806        gcp_labels: label_key, label_value
807        aws_tag/azure_tag: tag_key, tag_label
808        '''
809        if len(user_tags) != len(current_tags):
810            return True
811        tag_label_prefix = 'tag' if tag_name != 'gcp_labels' else 'label'
812        tkey = tag_label_prefix + '_key'
813        tvalue = tag_label_prefix + '_value'
814        # Check if tags/labels of desired configuration in current working environment
815        for item in user_tags:
816            if item[tkey] in current_tags and item[tvalue] != current_tags[item[tkey]]:
817                return True
818            elif item[tkey] not in current_tags:
819                return True
820        return False
821
822    def is_cvo_tags_changed(self, rest_api, headers, parameters, tag_name):
823        '''
824        Since tags/laabels are CVO optional parameters, this function needs to cover with/without tags/labels on both lists
825        '''
826        # get working environment details by working environment ID
827        current, error = self.get_working_environment_details(rest_api, headers)
828        if error is not None:
829            return 'Error:  Cannot find working environment %s error: %s' % (self.parameters['working_environment_id'], str(error))
830        self.set_api_root_path(current, rest_api)
831        # compare tags
832        # no tags in current cvo
833        if 'userTags' not in current:
834            return tag_name in parameters
835
836        # no tags in input parameters
837        if tag_name not in parameters:
838            return True
839        else:
840            # has tags in input parameters and existing CVO
841            return self.compare_cvo_tags_labels(current['userTags'], parameters[tag_name], tag_name)
842
843    def get_modify_cvo_params(self, rest_api, headers, desired, provider):
844        modified = ['svm_password']
845        # Get current working environment property
846        we, err = self.get_working_environment_property(rest_api, headers)
847        tier_level = we['ontapClusterProperties']['capacityTierInfo']['tierLevel']
848
849        # collect changed attributes
850        if provider == 'azure':
851            if desired['capacity_tier'] == 'Blob' and tier_level != desired['tier_level']:
852                modified.append('tier_level')
853        elif tier_level != desired['tier_level']:
854            modified.append('tier_level')
855
856        if provider == 'aws' and self.is_cvo_tags_changed(rest_api, headers, desired, 'aws_tag'):
857            modified.append('aws_tag')
858        if provider == 'azure' and self.is_cvo_tags_changed(rest_api, headers, desired, 'azure_tag'):
859            modified.append('azure_tag')
860        if provider == 'gcp' and self.is_cvo_tags_changed(rest_api, headers, desired, 'gcp_labels'):
861            modified.append('gcp_labels')
862
863        # The updates of followings are not supported. Will response failure.
864        for key, value in desired.items():
865            if key == 'project_id' and we['providerProperties']['projectName'] != value:
866                modified.append('project_id')
867            if key == 'zone' and we['providerProperties']['zoneName'][0] != value:
868                modified.append('zone')
869            if key == 'writing_speed_state' and we['ontapClusterProperties']['writingSpeedState'] is not None and \
870                    we['ontapClusterProperties']['writingSpeedState'] != value:
871                modified.append('writing_speed_state')
872            if key == 'cidr' and we['providerProperties']['vnetCidr'] != value:
873                modified.append('cidr')
874            if key == 'location' and we['providerProperties']['regionName'] != value:
875                modified.append('location')
876
877        if modified:
878            self.changed = True
879        return modified
880
881    def is_cvo_update_needed(self, rest_api, headers, parameters, changeable_params, provider):
882        modify = self.get_modify_cvo_params(rest_api, headers, parameters, provider)
883        unmodifiable = [attr for attr in modify if attr not in changeable_params]
884        if unmodifiable:
885            return None, "%s cannot be modified." % str(unmodifiable)
886        else:
887            return modify, None
888
889    def update_cvo_tags(self, base_url, rest_api, headers, tag_name, tag_list):
890        body = {}
891        tags = []
892        if tag_list is not None:
893            for tag in tag_list:
894                atag = {
895                    'tagKey': tag['label_key'] if tag_name == "gcp_labels" else tag['tag_key'],
896                    'tagValue': tag['label_value'] if tag_name == "gcp_labels" else tag['tag_value']
897                }
898                tags.append(atag)
899        body['tags'] = tags
900
901        response, err, dummy = rest_api.put(base_url + "user-tags", body, header=headers)
902        if err is not None:
903            return False, 'Error: unexpected response on modifying tags: %s, %s' % (str(err), str(response))
904        else:
905            return True, None
906
907    def update_svm_password(self, base_url, rest_api, headers, svm_password):
908        body = {'password': svm_password}
909        response, err, dummy = rest_api.put(base_url + "set-password", body, header=headers)
910        if err is not None:
911            return False, 'Error: unexpected response on modifying svm_password: %s, %s' % (str(err), str(response))
912        else:
913            return True, None
914
915    def update_tier_level(self, base_url, rest_api, headers, tier_level):
916        body = {'level': tier_level}
917        response, err, dummy = rest_api.post(base_url + "change-tier-level", body, header=headers)
918        if err is not None:
919            return False, 'Error: unexpected response on modify tier_level: %s, %s' % (str(err), str(response))
920        else:
921            return True, None
922