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