Server IP : 85.214.239.14 / Your IP : 3.15.188.166 Web Server : Apache/2.4.62 (Debian) System : Linux h2886529.stratoserver.net 4.9.0 #1 SMP Tue Jan 9 19:45:01 MSK 2024 x86_64 User : www-data ( 33) PHP Version : 7.4.18 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, MySQL : OFF | cURL : OFF | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /lib/python3/dist-packages/ansible_collections/netapp/ontap/plugins/modules/ |
Upload File : |
#!/usr/bin/python # (c) 2018-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ''' na_ontap_interface ''' from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = ''' module: na_ontap_interface short_description: NetApp ONTAP LIF configuration extends_documentation_fragment: - netapp.ontap.netapp.na_ontap version_added: 2.6.0 author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com> description: - Creating / deleting and modifying the LIF. options: state: description: - Whether the specified interface should exist or not. choices: ['present', 'absent'] default: 'present' type: str interface_name: description: - Specifies the logical interface (LIF) name. required: true type: str home_node: description: - Specifies the LIF's home node. - By default, the first node from the cluster is considered as home node. type: str current_node: description: - Specifies the LIF's current node. - By default, this is home_node type: str home_port: description: - Specifies the LIF's home port. - Requires ONTAP 9.8 or later with FC interfaces when using REST. - With REST, at least one of home_port, home_node, or broadcast_domain is required to create IP interfaces. - With REST, either home_port or current_port is required to create FC interfaces. - With ZAPI, home_port is required to create IP and FC interfaces. - home_port and broadcast_domain are mutually exclusive (REST and IP interfaces). type: str current_port: description: - Specifies the LIF's current port. type: str role: description: - Specifies the role of the LIF. - When setting role as "intercluster" or "cluster", setting protocol is not supported. - When creating a "cluster" role, the node name will appear as the prefix in the name of LIF. - For example, if the specified name is clif and node name is node1, the LIF name appears in the ONTAP as node1_clif. - Possible values are 'undef', 'cluster', 'data', 'node-mgmt', 'intercluster', 'cluster-mgmt'. - Required when C(state=present) unless service_policy is present and ONTAP version is 9.8 or better. - This option is deprecated in REST. - With REST, the module tries to derive a service_policy and may error out. type: str address: description: - Specifies the LIF's IP address. - ZAPI - Required when C(state=present) and is_ipv4_link_local if false and subnet_name is not set. - REST - Required when C(state=present) and C(interface_type) is IP. type: str netmask: description: - Specifies the LIF's netmask. - ZAPI - Required when C(state=present) and is_ipv4_link_local if false and subnet_name is not set. - REST - Required when C(state=present) and C(interface_type) is IP. type: str is_ipv4_link_local: description: - Specifies the LIF's are to acquire a ipv4 link local address. - Use case for this is when creating Cluster LIFs to allow for auto assignment of ipv4 link local address. - Not supported in REST version_added: '20.1.0' type: bool vserver: description: - The name of the vserver to use. - Required with ZAPI. - Required with REST for FC interfaces (data vservers). - Required with REST for SVM-scoped IP interfaces (data vservers). - Invalid with REST for cluster-scoped IP interfaces. - To help with transition from ZAPI to REST, vserver is ignored when the role is set to 'cluster', 'node-mgmt', 'intercluster', 'cluster-mgmt'. - Remove this option to suppress the warning. required: false type: str firewall_policy: description: - Specifies the firewall policy for the LIF. - This option is deprecated in REST. - With REST, the module tries to derive a service_policy and may error out. type: str failover_policy: description: - Specifies the failover policy for the LIF. - When using REST, this values are mapped to 'home_port_only', 'default', 'home_node_only', 'sfo_partners_only', 'broadcast_domain_only'. choices: ['disabled', 'system-defined', 'local-only', 'sfo-partner-only', 'broadcast-domain-wide'] type: str failover_scope: description: - Specifies the failover scope for the LIF. - REST only, and only for IP interfaces. Not supported for FC interfaces. choices: ['home_port_only', 'default', 'home_node_only', 'sfo_partners_only', 'broadcast_domain_only'] type: str version_added: '21.13.0' failover_group: description: - Specifies the failover group for the LIF. - Not supported with REST. version_added: '20.1.0' type: str subnet_name: description: - Subnet where the IP interface address is allocated from. - If the option is not used, the IP address and netmask need to be provided. - With REST, ONTAP 9.11.1 or later is required. - With REST, ipspace must be set. version_added: 2.8.0 type: str fail_if_subnet_conflicts: description: - Creating or updating an IP Interface fails if the specified IP address falls within the address range of a named subnet. - Set this value to false to use the specified IP address and to assign the subnet owning that address to the interface. - This option is only supported with REST and requires ONTAP 9.11.1 or later. version_added: 22.2.0 type: bool admin_status: choices: ['up', 'down'] description: - Specifies the administrative status of the LIF. type: str is_auto_revert: description: - If true, data LIF will revert to its home node under certain circumstances such as startup, - and load balancing migration capability is disabled automatically type: bool force_subnet_association: description: - Set this to true to acquire the address from the named subnet and assign the subnet to the LIF. - not supported with REST. version_added: 2.9.0 type: bool protocols: description: - Specifies the list of data protocols configured on the LIF. By default, the values in this element are nfs, cifs and fcache. - Other supported protocols are iscsi and fcp. A LIF can be configured to not support any data protocols by specifying 'none'. - Protocol values of none, iscsi, fc-nvme or fcp can't be combined with any other data protocol(s). - address, netmask and firewall_policy parameters are not supported for 'fc-nvme' option. - This option is ignored with REST, though it can be used to derive C(interface_type) or C(data_protocol). type: list elements: str data_protocol: description: - The data protocol for which the FC interface is configured. - Ignored with ZAPI or for IP interfaces. - Required to create a FC type interface. type: str choices: ['fcp', 'fc_nvme'] dns_domain_name: description: - Specifies the unique, fully qualified domain name of the DNS zone of this LIF. - Supported from ONTAP 9.9.0 or later in REST. - Not supported for FC interfaces. version_added: 2.9.0 type: str listen_for_dns_query: description: - If True, this IP address will listen for DNS queries for the dnszone specified. - Not supported with REST. version_added: 2.9.0 type: bool is_dns_update_enabled: description: - Specifies if DNS update is enabled for this LIF. Dynamic updates will be sent for this LIF if updates are enabled at Vserver level. - Supported from ONTAP 9.9.1 or later in REST. - Not supported for FC interfaces. version_added: 2.9.0 type: bool service_policy: description: - Starting with ONTAP 9.5, you can configure LIF service policies to identify a single service or a list of services that will use a LIF. - In ONTAP 9.5, you can assign service policies only for LIFs in the admin SVM. - In ONTAP 9.6, you can additionally assign service policies for LIFs in the data SVMs. - When you specify a service policy for a LIF, you need not specify the data protocol and role for the LIF. - NOTE that role is still required because of a ZAPI issue. This limitation is removed in ONTAP 9.8. - Creating LIFs by specifying the role and data protocols is also supported. version_added: '20.4.0' type: str from_name: description: name of the interface to be renamed type: str version_added: 21.11.0 interface_type: description: - type of the interface. - IP is assumed if address or netmask are present. - IP interfaces includes cluster, intercluster, management, and NFS, CIFS, iSCSI interfaces. - FC interfaces includes FCP and NVME-FC interfaces. - ignored with ZAPI. - required with REST, but maybe derived from deprecated options like C(role), C(protocols), and C(firewall_policy). type: str choices: ['fc', 'ip'] version_added: 21.13.0 ipspace: description: - IPspace name is required with REST for cluster-scoped interfaces. It is optional with SVM scope. - ignored with ZAPI. - ignored for FC interface. type: str version_added: 21.13.0 broadcast_domain: description: - broadcast_domain name can be used to specify the location on an IP interface with REST, as an alternative to node or port. - only used when creating an IP interface to select a node, ignored if the interface already exists. - if the broadcast domain is not found, make sure to check the ipspace value. - home_port and broadcast_domain are mutually exclusive. home_node may or may not be present. - not supported for FC interface. - ignored with ZAPI. type: str version_added: 21.21.0 ignore_zapi_options: description: - ignore unsupported options that should not be relevant. - ignored with ZAPI. choices: ['failover_group', 'force_subnet_association', 'listen_for_dns_query'] type: list elements: str default: ['force_subnet_association'] version_added: 21.13.0 probe_port: description: - Probe port for Cloud load balancer - only valid in the Azure environment. - Not supported with ZAPI or with FC interfaces. - Requires ONTAP 9.10.1 or later. type: int version_added: 22.1.0 notes: - REST support requires ONTAP 9.7 or later. - Support check_mode. ''' EXAMPLES = ''' - name: Create interface - ZAPI netapp.ontap.na_ontap_interface: state: present interface_name: data2 home_port: e0d home_node: laurentn-vsim1 role: data protocols: - nfs - cifs admin_status: up failover_policy: local-only firewall_policy: mgmt is_auto_revert: true address: 10.10.10.10 netmask: 255.255.255.0 force_subnet_association: false dns_domain_name: test.com listen_for_dns_query: true is_dns_update_enabled: true vserver: svm1 hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" - name: Create data interface - REST - NAS netapp.ontap.na_ontap_interface: state: present interface_name: data2 home_port: e0d home_node: laurentn-vsim1 admin_status: up failover_scope: home_node_only service_policy: default-data-files is_auto_revert: true interface_type: ip address: 10.10.10.10 netmask: 255.255.255.0 vserver: svm1 hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" - name: Create cluster interface - ZAPI netapp.ontap.na_ontap_interface: state: present interface_name: cluster_lif home_port: e0a home_node: cluster1-01 role: cluster admin_status: up is_auto_revert: true is_ipv4_link_local: true vserver: Cluster hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" - name: Create cluster interface - REST netapp.ontap.na_ontap_interface: state: present interface_name: cluster_lif home_port: e0a home_node: cluster1-01 service_policy: default-cluster admin_status: up is_auto_revert: true vserver: Cluster hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" - name: Rename interface netapp.ontap.na_ontap_interface: state: present from_name: ansibleSVM_lif interface_name: ansibleSVM_lif01 vserver: ansibleSVM hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" - name: Migrate an interface netapp.ontap.na_ontap_interface: hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" vserver: ansible https: true validate_certs: false state: present interface_name: carchi_interface3 home_port: e0d home_node: ansdev-stor-1 current_node: ansdev-stor-2 role: data failover_policy: local-only firewall_policy: mgmt is_auto_revert: true address: 10.10.10.12 netmask: 255.255.255.0 force_subnet_association: false admin_status: up - name: Delete interface netapp.ontap.na_ontap_interface: state: absent interface_name: data2 vserver: svm1 hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" ''' RETURN = """ """ import time import traceback from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_native from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils from ansible_collections.netapp.ontap.plugins.module_utils.netapp import OntapRestAPI from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic, netapp_ipaddress FAILOVER_POLICIES = ['disabled', 'system-defined', 'local-only', 'sfo-partner-only', 'broadcast-domain-wide'] FAILOVER_SCOPES = ['home_port_only', 'default', 'home_node_only', 'sfo_partners_only', 'broadcast_domain_only'] REST_UNSUPPORTED_OPTIONS = ['is_ipv4_link_local'] REST_IGNORABLE_OPTIONS = ['failover_group', 'force_subnet_association', 'listen_for_dns_query'] class NetAppOntapInterface: ''' object to describe interface info ''' def __init__(self): self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update(dict( state=dict(required=False, choices=[ 'present', 'absent'], default='present'), interface_name=dict(required=True, type='str'), interface_type=dict(type='str', choices=['fc', 'ip']), ipspace=dict(type='str'), broadcast_domain=dict(type='str'), home_node=dict(required=False, type='str', default=None), current_node=dict(required=False, type='str'), home_port=dict(required=False, type='str'), current_port=dict(required=False, type='str'), role=dict(required=False, type='str'), is_ipv4_link_local=dict(required=False, type='bool', default=None), address=dict(required=False, type='str'), netmask=dict(required=False, type='str'), vserver=dict(required=False, type='str'), firewall_policy=dict(required=False, type='str', default=None), failover_policy=dict(required=False, type='str', default=None, choices=['disabled', 'system-defined', 'local-only', 'sfo-partner-only', 'broadcast-domain-wide']), failover_scope=dict(required=False, type='str', default=None, choices=['home_port_only', 'default', 'home_node_only', 'sfo_partners_only', 'broadcast_domain_only']), failover_group=dict(required=False, type='str'), admin_status=dict(required=False, choices=['up', 'down']), subnet_name=dict(required=False, type='str'), is_auto_revert=dict(required=False, type='bool', default=None), protocols=dict(required=False, type='list', elements='str'), data_protocol=dict(required=False, type='str', choices=['fc_nvme', 'fcp']), force_subnet_association=dict(required=False, type='bool', default=None), dns_domain_name=dict(required=False, type='str'), listen_for_dns_query=dict(required=False, type='bool'), is_dns_update_enabled=dict(required=False, type='bool'), service_policy=dict(required=False, type='str', default=None), from_name=dict(required=False, type='str'), ignore_zapi_options=dict(required=False, type='list', elements='str', default=['force_subnet_association'], choices=REST_IGNORABLE_OPTIONS), probe_port=dict(required=False, type='int'), fail_if_subnet_conflicts=dict(required=False, type='bool'), )) self.module = AnsibleModule( argument_spec=self.argument_spec, mutually_exclusive=[ ['subnet_name', 'address'], ['subnet_name', 'netmask'], ['is_ipv4_link_local', 'address'], ['is_ipv4_link_local', 'netmask'], ['is_ipv4_link_local', 'subnet_name'], ['failover_policy', 'failover_scope'], ], supports_check_mode=True ) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) self.rest_api = OntapRestAPI(self.module) unsupported_rest_properties = [key for key in REST_IGNORABLE_OPTIONS if key not in self.parameters['ignore_zapi_options']] unsupported_rest_properties.extend(REST_UNSUPPORTED_OPTIONS) if self.na_helper.safe_get(self.parameters, ['address']): self.parameters['address'] = netapp_ipaddress.validate_and_compress_ip_address(self.parameters['address'], self.module) partially_supported_rest_properties = [['dns_domain_name', (9, 9, 0)], ['is_dns_update_enabled', (9, 9, 1)], ['probe_port', (9, 10, 1)], ['subnet_name', (9, 11, 1)], ['fail_if_subnet_conflicts', (9, 11, 1)]] self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, unsupported_rest_properties, partially_supported_rest_properties) if self.use_rest and not self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 7, 0): msg = 'REST requires ONTAP 9.7 or later for interface APIs.' self.use_rest = self.na_helper.fall_back_to_zapi(self.module, msg, self.parameters) if self.use_rest: self.cluster_nodes = None # cached value to limit number of API calls. self.home_node = None # cached value to limit number of API calls. self.map_failover_policy() self.validate_rest_input_parameters() # REST supports both netmask and cidr for ipv4 but cidr only for ipv6. if self.parameters.get('netmask'): self.parameters['netmask'] = str(netapp_ipaddress.netmask_to_netmask_length(self.parameters.get('address'), self.parameters['netmask'], self.module)) elif netapp_utils.has_netapp_lib() is False: self.module.fail_json(msg=netapp_utils.netapp_lib_is_required()) else: for option in ('probe_port', 'fail_if_subnet_conflicts'): if self.parameters.get(option) is not None: self.module.fail_json(msg='Error option %s requires REST.' % option) if 'vserver' not in self.parameters: self.module.fail_json(msg='missing required argument with ZAPI: vserver') self.server = netapp_utils.setup_na_ontap_zapi(module=self.module) # ZAPI supports only netmask. if self.parameters.get('netmask'): self.parameters['netmask'] = netapp_ipaddress.netmask_length_to_netmask(self.parameters.get('address'), self.parameters['netmask'], self.module) def map_failover_policy(self): if self.use_rest and 'failover_policy' in self.parameters: mapping = dict(zip(FAILOVER_POLICIES, FAILOVER_SCOPES)) self.parameters['failover_scope'] = mapping[self.parameters['failover_policy']] def set_interface_type(self, interface_type): if 'interface_type' in self.parameters: if self.parameters['interface_type'] != interface_type: self.module.fail_json(msg="Error: mismatch between configured interface_type: %s and derived interface_type: %s." % (self.parameters['interface_type'], interface_type)) else: self.parameters['interface_type'] = interface_type def derive_fc_data_protocol(self): protocols = self.parameters.get('protocols') if not protocols: return if len(protocols) > 1: self.module.fail_json(msg="A single protocol entry is expected for FC interface, got %s." % protocols) mapping = {'fc-nvme': 'fc_nvme', 'fc_nvme': 'fc_nvme', 'fcp': 'fcp'} if protocols[0] not in mapping: self.module.fail_json(msg="Unexpected protocol value %s." % protocols[0]) data_protocol = mapping[protocols[0]] if 'data_protocol' in self.parameters and self.parameters['data_protocol'] != data_protocol: self.module.fail_json(msg="Error: mismatch between configured data_protocol: %s and data_protocols: %s" % (self.parameters['data_protocol'], protocols)) self.parameters['data_protocol'] = data_protocol def derive_interface_type(self): protocols = self.parameters.get('protocols') if protocols in (None, ["none"]): if self.parameters.get('role') in ('cluster', 'intercluster') or any(x in self.parameters for x in ('address', 'netmask', 'subnet_name')): self.set_interface_type('ip') return protocol_types = set() unknown_protocols = [] for protocol in protocols: if protocol.lower() in ['fc-nvme', 'fcp']: protocol_types.add('fc') elif protocol.lower() in ['nfs', 'cifs', 'iscsi']: protocol_types.add('ip') elif protocol.lower() != 'none': # none is an allowed value with ZAPI unknown_protocols.append(protocol) errors = [] if unknown_protocols: errors.append('unexpected value(s) for protocols: %s' % unknown_protocols) if len(protocol_types) > 1: errors.append('incompatible value(s) for protocols: %s' % protocols) if errors: self.module.fail_json(msg='Error: unable to determine interface type, please set interface_type: %s' % (' - '.join(errors))) if protocol_types: self.set_interface_type(protocol_types.pop()) return def derive_block_file_type(self, protocols): block_p, file_p, fcp = False, False, False if protocols is None: fcp = self.parameters.get('interface_type') == 'fc' return fcp, file_p, fcp block_values, file_values = [], [] for protocol in protocols: if protocol.lower() in ['fc-nvme', 'fcp', 'iscsi']: block_p = True block_values.append(protocol) if protocol.lower() in ['fc-nvme', 'fcp']: fcp = True elif protocol.lower() in ['nfs', 'cifs']: file_p = True file_values.append(protocol) if block_p and file_p: self.module.fail_json(msg="Cannot use any of %s with %s" % (block_values, file_values)) return block_p, file_p, fcp def get_interface_record_rest(self, if_type, query, fields): if 'ipspace' in self.parameters and if_type == 'ip': query['ipspace.name'] = self.parameters['ipspace'] return rest_generic.get_one_record(self.rest_api, self.get_net_int_api(if_type), query, fields) def get_interface_records_rest(self, if_type, query, fields): if 'ipspace' in self.parameters: if if_type == 'ip': query['ipspace.name'] = self.parameters['ipspace'] else: self.module.warn("ipspace is ignored for FC interfaces.") records, error = rest_generic.get_0_or_more_records(self.rest_api, self.get_net_int_api(if_type), query, fields) if error and 'are available in precluster.' in error: # in precluster mode, network APIs are not available! self.module.fail_json(msg="This module cannot use REST in precluster mode, ZAPI can be forced with use_rest: never. Error: %s" % error) return records, error def get_net_int_api(self, if_type=None): if if_type is None: if_type = self.parameters.get('interface_type') if if_type is None: self.module.fail_json(msg='Error: missing option "interface_type (or could not be derived)') return 'network/%s/interfaces' % if_type def find_interface_record(self, records, home_node, name): full_name = "%s_%s" % (home_node, name) if home_node is not None else name full_name_records = [record for record in records if record['name'] == full_name] if len(full_name_records) > 1: self.module.fail_json(msg='Error: multiple records for: %s - %s' % (full_name, full_name_records)) return full_name_records[0] if full_name_records else None def find_exact_match(self, records, name): """ with vserver, we expect an exact match but ONTAP transforms cluster interface names by prepending the home_port """ if 'vserver' in self.parameters: if len(records) > 1: self.module.fail_json(msg='Error: unexpected records for name: %s, vserver: %s - %s' % (name, self.parameters['vserver'], records)) return records[0] if records else None # since our queries included a '*', we expect multiple records # an exact match is <home_node>_<name> or <name>. # is there an exact macth on name only? record = self.find_interface_record(records, None, name) # now matching with home_port as a prefix if 'home_node' in self.parameters and self.parameters['home_node'] != 'localhost': home_record = self.find_interface_record(records, self.parameters['home_node'], name) if record and home_record: self.module.warn('Found both %s, selecting %s' % ([record['name'] for record in (record, home_record)], home_record['name'])) else: # look for all known nodes home_node_records = [] for home_node in self.get_cluster_node_names_rest(): home_record = self.find_interface_record(records, home_node, name) if home_record: home_node_records.append(home_record) if len(home_node_records) > 1: self.module.fail_json(msg='Error: multiple matches for name: %s: %s. Set home_node parameter.' % (name, [record['name'] for record in home_node_records])) home_record = home_node_records[0] if home_node_records else None if record and home_node_records: self.module.fail_json(msg='Error: multiple matches for name: %s: %s. Set home_node parameter.' % (name, [record['name'] for record in (record, home_record)])) if home_record: record = home_record if record and name == self.parameters['interface_name'] and name != record['name']: # fix name, otherwise we'll attempt a rename :( self.parameters['interface_name'] = record['name'] self.module.warn('adjusting name from %s to %s' % (name, record['name'])) return record def get_interface_rest(self, name): """ Return details about the interface :param: name : Name of the interface :return: Details about the interface. None if not found. :rtype: dict """ self.derive_interface_type() if_type = self.parameters.get('interface_type') if 'vserver' in self.parameters: query_ip = { 'name': name, 'svm.name': self.parameters['vserver'] } query_fc = query_ip else: query_ip = { # ONTAP renames cluster interfaces, use a * to find them 'name': '*%s' % name, 'scope': 'cluster' } query_fc = None fields = 'name,location,uuid,enabled,svm.name' fields_fc = fields + ',data_protocol' fields_ip = fields + ',ip,service_policy' if self.parameters.get('dns_domain_name'): fields_ip += ',dns_zone' if self.parameters.get('probe_port') is not None: fields_ip += ',probe_port' if self.parameters.get('is_dns_update_enabled') is not None: fields_ip += ',ddns_enabled' if self.parameters.get('subnet_name') is not None: fields_ip += ',subnet' records, error, records2, error2 = None, None, None, None if if_type in [None, 'ip']: records, error = self.get_interface_records_rest('ip', query_ip, fields_ip) if if_type in [None, 'fc'] and query_fc: records2, error2 = self.get_interface_records_rest('fc', query_fc, fields_fc) if records and records2: msg = 'Error fetching interface %s - found duplicate entries, please indicate interface_type.' % name msg += ' - ip interfaces: %s' % records msg += ' - fc interfaces: %s' % records2 self.module.fail_json(msg=msg) if error is None and error2 is not None and records: # ignore error on fc if ip interface is found error2 = None if error2 is None and error is not None and records2: # ignore error on ip if fc interface is found error = None if error or error2: errors = [to_native(err) for err in (error, error2) if err] self.module.fail_json(msg='Error fetching interface details for %s: %s' % (name, ' - '.join(errors)), exception=traceback.format_exc()) if records: self.set_interface_type('ip') if records2: self.set_interface_type('fc') records = records2 record = self.find_exact_match(records, name) if records else None return self.dict_from_record(record) def dict_from_record(self, record): if not record: return None # Note: broadcast_domain is CreateOnly return_value = { 'interface_name': record['name'], 'interface_type': self.parameters['interface_type'], 'uuid': record['uuid'], 'admin_status': 'up' if record['enabled'] else 'down', } # home_node/home_port not present for FC on ONTAP 9.7. if self.na_helper.safe_get(record, ['location', 'home_node', 'name']): return_value['home_node'] = record['location']['home_node']['name'] if self.na_helper.safe_get(record, ['location', 'home_port', 'name']): return_value['home_port'] = record['location']['home_port']['name'] if self.na_helper.safe_get(record, ['svm', 'name']): return_value['vserver'] = record['svm']['name'] if 'data_protocol' in record: return_value['data_protocol'] = record['data_protocol'] if 'auto_revert' in record['location']: return_value['is_auto_revert'] = record['location']['auto_revert'] if 'failover' in record['location']: return_value['failover_scope'] = record['location']['failover'] # if interface_attributes.get_child_by_name('failover-group'): # return_value['failover_group'] = interface_attributes['failover-group'] if self.na_helper.safe_get(record, ['ip', 'address']): return_value['address'] = netapp_ipaddress.validate_and_compress_ip_address(record['ip']['address'], self.module) if self.na_helper.safe_get(record, ['ip', 'netmask']) is not None: return_value['netmask'] = record['ip']['netmask'] if self.na_helper.safe_get(record, ['service_policy', 'name']): return_value['service_policy'] = record['service_policy']['name'] if self.na_helper.safe_get(record, ['location', 'node', 'name']): return_value['current_node'] = record['location']['node']['name'] if self.na_helper.safe_get(record, ['location', 'port', 'name']): return_value['current_port'] = record['location']['port']['name'] if self.na_helper.safe_get(record, ['dns_zone']): return_value['dns_domain_name'] = record['dns_zone'] if self.na_helper.safe_get(record, ['probe_port']) is not None: return_value['probe_port'] = record['probe_port'] if 'ddns_enabled' in record: return_value['is_dns_update_enabled'] = record['ddns_enabled'] if self.na_helper.safe_get(record, ['subnet', 'name']): return_value['subnet_name'] = record['subnet']['name'] return return_value def get_node_port(self, uuid): record, error = self.get_interface_record_rest(self.parameters['interface_type'], {'uuid': uuid}, 'location') if error or not record: return None, None, error node = self.na_helper.safe_get(record, ['location', 'node', 'name']) port = self.na_helper.safe_get(record, ['location', 'port', 'name']) return node, port, None def get_interface(self, name=None): """ Return details about the interface :param: name : Name of the interface :return: Details about the interface. None if not found. :rtype: dict """ if name is None: name = self.parameters['interface_name'] if self.use_rest: return self.get_interface_rest(name) interface_info = netapp_utils.zapi.NaElement('net-interface-get-iter') interface_attributes = netapp_utils.zapi.NaElement('net-interface-info') interface_attributes.add_new_child('interface-name', name) interface_attributes.add_new_child('vserver', self.parameters['vserver']) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(interface_attributes) interface_info.add_child_elem(query) try: result = self.server.invoke_successfully(interface_info, True) except netapp_utils.zapi.NaApiError as exc: self.module.fail_json(msg='Error fetching interface details for %s: %s' % (name, to_native(exc)), exception=traceback.format_exc()) return_value = None if result.get_child_by_name('num-records') and \ int(result.get_child_content('num-records')) >= 1: interface_attributes = result.get_child_by_name('attributes-list'). \ get_child_by_name('net-interface-info') return_value = { 'interface_name': name, 'admin_status': interface_attributes['administrative-status'], 'home_port': interface_attributes['home-port'], 'home_node': interface_attributes['home-node'], 'failover_policy': interface_attributes['failover-policy'].replace('_', '-'), } if interface_attributes.get_child_by_name('is-auto-revert'): return_value['is_auto_revert'] = (interface_attributes['is-auto-revert'] == 'true') if interface_attributes.get_child_by_name('failover-group'): return_value['failover_group'] = interface_attributes['failover-group'] if interface_attributes.get_child_by_name('address'): return_value['address'] = netapp_ipaddress.validate_and_compress_ip_address(interface_attributes['address'], self.module) if interface_attributes.get_child_by_name('netmask'): return_value['netmask'] = interface_attributes['netmask'] if interface_attributes.get_child_by_name('firewall-policy'): return_value['firewall_policy'] = interface_attributes['firewall-policy'] if interface_attributes.get_child_by_name('dns-domain-name') not in ('none', None): return_value['dns_domain_name'] = interface_attributes['dns-domain-name'] else: return_value['dns_domain_name'] = None if interface_attributes.get_child_by_name('listen-for-dns-query'): return_value['listen_for_dns_query'] = self.na_helper.get_value_for_bool(True, interface_attributes[ 'listen-for-dns-query']) if interface_attributes.get_child_by_name('is-dns-update-enabled'): return_value['is_dns_update_enabled'] = self.na_helper.get_value_for_bool(True, interface_attributes[ 'is-dns-update-enabled']) if interface_attributes.get_child_by_name('is-ipv4-link-local'): return_value['is_ipv4_link_local'] = self.na_helper.get_value_for_bool(True, interface_attributes[ 'is-ipv4-link-local']) if interface_attributes.get_child_by_name('service-policy'): return_value['service_policy'] = interface_attributes['service-policy'] if interface_attributes.get_child_by_name('current-node'): return_value['current_node'] = interface_attributes['current-node'] if interface_attributes.get_child_by_name('current-port'): return_value['current_port'] = interface_attributes['current-port'] return return_value @staticmethod def set_options(options, parameters): """ set attributes for create or modify """ if parameters.get('role') is not None: options['role'] = parameters['role'] if parameters.get('home_node') is not None: options['home-node'] = parameters['home_node'] if parameters.get('home_port') is not None: options['home-port'] = parameters['home_port'] if parameters.get('subnet_name') is not None: options['subnet-name'] = parameters['subnet_name'] if parameters.get('address') is not None: options['address'] = parameters['address'] if parameters.get('netmask') is not None: options['netmask'] = parameters['netmask'] if parameters.get('failover_policy') is not None: options['failover-policy'] = parameters['failover_policy'] if parameters.get('failover_group') is not None: options['failover-group'] = parameters['failover_group'] if parameters.get('firewall_policy') is not None: options['firewall-policy'] = parameters['firewall_policy'] if parameters.get('is_auto_revert') is not None: options['is-auto-revert'] = 'true' if parameters['is_auto_revert'] else 'false' if parameters.get('admin_status') is not None: options['administrative-status'] = parameters['admin_status'] if parameters.get('force_subnet_association') is not None: options['force-subnet-association'] = 'true' if parameters['force_subnet_association'] else 'false' if parameters.get('dns_domain_name') is not None: options['dns-domain-name'] = parameters['dns_domain_name'] if parameters.get('listen_for_dns_query') is not None: options['listen-for-dns-query'] = 'true' if parameters['listen_for_dns_query'] else 'false' if parameters.get('is_dns_update_enabled') is not None: options['is-dns-update-enabled'] = 'true' if parameters['is_dns_update_enabled'] else 'false' if parameters.get('is_ipv4_link_local') is not None: options['is-ipv4-link-local'] = 'true' if parameters['is_ipv4_link_local'] else 'false' if parameters.get('service_policy') is not None: options['service-policy'] = parameters['service_policy'] def fix_errors(self, options, errors): '''ignore role and firewall_policy if a service_policy can be safely derived''' block_p, file_p, fcp = self.derive_block_file_type(self.parameters.get('protocols')) if 'role' in errors: fixed = False if errors['role'] == 'data' and errors.get('firewall_policy', 'data') == 'data': if fcp: # service_policy is not supported for FC interfaces fixed = True elif file_p and self.parameters.get('service_policy', 'default-data-files') == 'default-data-files': options['service_policy'] = 'default-data-files' fixed = True elif block_p and self.parameters.get('service_policy', 'default-data-blocks') == 'default-data-blocks': options['service_policy'] = 'default-data-blocks' fixed = True if errors['role'] == 'data' and errors.get('firewall_policy') == 'mgmt': options['service_policy'] = 'default-management' fixed = True if errors['role'] == 'intercluster' and errors.get('firewall_policy') in [None, 'intercluster']: options['service_policy'] = 'default-intercluster' fixed = True if errors['role'] == 'cluster' and errors.get('firewall_policy') in [None, 'mgmt']: options['service_policy'] = 'default-cluster' fixed = True if errors['role'] == 'data' and fcp and errors.get('firewall_policy') is None: # ignore role for FC interface fixed = True if fixed: errors.pop('role') errors.pop('firewall_policy', None) def set_options_rest(self, parameters): """ set attributes for create or modify """ def add_ip(options, key, value): if 'ip' not in options: options['ip'] = {} options['ip'][key] = value def add_location(options, key, value, node=None): if 'location' not in options: options['location'] = {} # Note: broadcast_domain is CreateOnly if key in ['home_node', 'home_port', 'node', 'port', 'broadcast_domain']: options['location'][key] = {'name': value} else: options['location'][key] = value if key in ['home_port', 'port']: options['location'][key]['node'] = {'name': node} def get_node_for_port(parameters, pkey): if pkey == 'current_port': return parameters.get('current_node') or self.parameters.get('home_node') or self.get_home_node_for_cluster() elif pkey == 'home_port': return self.parameters.get('home_node') or self.get_home_node_for_cluster() else: return None options, migrate_options, errors = {}, {}, {} # We normally create using home_port, and migrate to current. # But for FC, home_port is not supported on 9.7 or earlier! create_with_current = False if parameters is None: parameters = self.parameters if self.parameters['interface_type'] == 'fc' and 'home_port' not in self.parameters: create_with_current = True mapping_params_to_rest = { 'admin_status': 'enabled', 'interface_name': 'name', 'vserver': 'svm.name', # LOCATION 'current_port': 'port', 'home_port': 'home_port' } if self.parameters['interface_type'] == 'ip': mapping_params_to_rest.update({ 'ipspace': 'ipspace.name', 'service_policy': 'service_policy', 'dns_domain_name': 'dns_zone', 'is_dns_update_enabled': 'ddns_enabled', 'probe_port': 'probe_port', 'subnet_name': 'subnet.name', 'fail_if_subnet_conflicts': 'fail_if_subnet_conflicts', # IP 'address': 'address', 'netmask': 'netmask', # LOCATION 'broadcast_domain': 'broadcast_domain', 'failover_scope': 'failover', 'is_auto_revert': 'auto_revert', # home_node/current_node supported only in ip interfaces. 'home_node': 'home_node', 'current_node': 'node' }) if self.parameters['interface_type'] == 'fc': mapping_params_to_rest['data_protocol'] = 'data_protocol' ip_keys = ('address', 'netmask') location_keys = ('home_port', 'home_node', 'current_port', 'current_node', 'failover_scope', 'is_auto_revert', 'broadcast_domain') # don't add node location when port structure is already present has_home_port, has_current_port = False, False if 'home_port' in parameters: has_home_port = True if 'current_port' in parameters: has_current_port = True for pkey, rkey in mapping_params_to_rest.items(): if pkey in parameters: if pkey == 'admin_status': options[rkey] = parameters[pkey] == 'up' elif pkey in ip_keys: add_ip(options, rkey, parameters[pkey]) elif pkey in location_keys: if has_home_port and pkey == 'home_node': continue if has_current_port and pkey == 'current_node': continue dest = migrate_options if rkey in ('node', 'port') and not create_with_current else options add_location(dest, rkey, parameters[pkey], get_node_for_port(parameters, pkey)) else: options[rkey] = parameters[pkey] keys_in_error = ('role', 'failover_group', 'firewall_policy', 'force_subnet_association', 'listen_for_dns_query', 'is_ipv4_link_local') for pkey in keys_in_error: if pkey in parameters: errors[pkey] = parameters[pkey] return options, migrate_options, errors def set_protocol_option(self, required_keys): """ set protocols for create """ if self.parameters.get('protocols') is None: return None data_protocols_obj = netapp_utils.zapi.NaElement('data-protocols') for protocol in self.parameters.get('protocols'): if protocol.lower() in ['fc-nvme', 'fcp']: if 'address' in required_keys: required_keys.remove('address') if 'home_port' in required_keys: required_keys.remove('home_port') if 'netmask' in required_keys: required_keys.remove('netmask') not_required_params = set(['address', 'netmask', 'firewall_policy']) if not not_required_params.isdisjoint(set(self.parameters.keys())): self.module.fail_json(msg='Error: Following parameters for creating interface are not supported' ' for data-protocol fc-nvme: %s' % ', '.join(not_required_params)) data_protocols_obj.add_new_child('data-protocol', protocol) return data_protocols_obj def get_cluster_node_names_rest(self): ''' get cluster node names, but the cluster may not exist yet return: empty list if the cluster cannot be reached a list of nodes ''' if self.cluster_nodes is None: records, error = rest_generic.get_0_or_more_records(self.rest_api, 'cluster/nodes', fields='name,uuid,cluster_interfaces') if error: self.module.fail_json(msg='Error fetching cluster node info: %s' % to_native(error), exception=traceback.format_exc()) self.cluster_nodes = records or [] return [record['name'] for record in self.cluster_nodes] def get_home_node_for_cluster(self): ''' get the first node name from this cluster ''' if self.use_rest: if not self.home_node: nodes = self.get_cluster_node_names_rest() if nodes: self.home_node = nodes[0] return self.home_node get_node = netapp_utils.zapi.NaElement('cluster-node-get-iter') attributes = { 'query': { 'cluster-node-info': {} } } get_node.translate_struct(attributes) try: result = self.server.invoke_successfully(get_node, enable_tunneling=True) except netapp_utils.zapi.NaApiError as exc: if str(exc.code) == '13003' or exc.message == 'ZAPI is not enabled in pre-cluster mode.': return None self.module.fail_json(msg='Error fetching node for interface %s: %s' % (self.parameters['interface_name'], to_native(exc)), exception=traceback.format_exc()) if result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1: attributes = result.get_child_by_name('attributes-list') return attributes.get_child_by_name('cluster-node-info').get_child_content('node-name') return None def validate_rest_input_parameters(self, action=None): if 'vserver' in self.parameters and self.parameters.get('role') in ['cluster', 'intercluster', 'node-mgmt', 'cluster-mgmt']: # REST only supports DATA SVMs del self.parameters['vserver'] self.module.warn('Ignoring vserver with REST for non data SVM.') errors = [] if action == 'create': if 'vserver' not in self.parameters and 'ipspace' not in self.parameters: errors.append('ipspace name must be provided if scope is cluster, or vserver for svm scope.') if self.parameters['interface_type'] == 'fc': unsupported_fc_options = ['broadcast_domain', 'dns_domain_name', 'is_dns_update_enabled', 'probe_port', 'subnet_name', 'fail_if_subnet_conflicts'] used_unsupported_fc_options = [option for option in unsupported_fc_options if option in self.parameters] if used_unsupported_fc_options: plural = 's' if len(used_unsupported_fc_options) > 1 else '' errors.append('%s option%s only supported for IP interfaces: %s, interface_type: %s' % (', '.join(used_unsupported_fc_options), plural, self.parameters.get('interface_name'), self.parameters['interface_type'])) if self.parameters.get('home_port') and self.parameters.get('broadcast_domain'): errors.append('home_port and broadcast_domain are mutually exclusive for creating: %s' % self.parameters.get('interface_name')) if self.parameters.get('role') == "intercluster" and self.parameters.get('protocols') is not None: errors.append('Protocol cannot be specified for intercluster role, failed to create interface.') if errors: self.module.fail_json(msg='Error: %s' % ' '.join(errors)) ignored_keys = [] for key in self.parameters.get('ignore_zapi_options', []): if key in self.parameters: del self.parameters[key] ignored_keys.append(key) if ignored_keys: self.module.warn("Ignoring %s" % ', '.join(ignored_keys)) # if role is intercluster, protocol cannot be specified def validate_required_parameters(self, keys): ''' Validate if required parameters for create or modify are present. Parameter requirement might vary based on given data-protocol. :return: None ''' home_node = self.parameters.get('home_node') or self.get_home_node_for_cluster() # validate if mandatory parameters are present for create or modify errors = [] if self.use_rest and home_node is None and self.parameters.get('home_port') is not None: errors.append('Cannot guess home_node, home_node is required when home_port is present with REST.') if 'broadcast_domain_home_port_or_home_node' in keys: if all(x not in self.parameters for x in ['broadcast_domain', 'home_port', 'home_node']): errors.append("At least one of 'broadcast_domain', 'home_port', 'home_node' is required to create an IP interface.") keys.remove('broadcast_domain_home_port_or_home_node') if not keys.issubset(set(self.parameters.keys())): errors.append('Missing one or more required parameters for creating interface: %s.' % ', '.join(keys)) if 'interface_type' in keys and 'interface_type' in self.parameters: if self.parameters['interface_type'] not in ['fc', 'ip']: errors.append('unexpected value for interface_type: %s.' % self.parameters['interface_type']) elif self.parameters['interface_type'] == 'fc': if not self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 8, 0): if 'home_port' in self.parameters: errors.append("'home_port' is not supported for FC interfaces with 9.7, use 'current_port', avoid home_node.") if 'home_node' in self.parameters: self.module.warn("Avoid 'home_node' with FC interfaces with 9.7, use 'current_node'.") if 'vserver' not in self.parameters: errors.append("A data 'vserver' is required for FC interfaces.") if 'service_policy' in self.parameters: errors.append("'service_policy' is not supported for FC interfaces.") if 'role' in self.parameters and self.parameters.get('role') != 'data': errors.append("'role' is deprecated, and 'data' is the only value supported for FC interfaces: found %s." % self.parameters.get('role')) if 'probe_port' in self.parameters: errors.append("'probe_port' is not supported for FC interfaces.") if errors: self.module.fail_json(msg='Error: %s' % ' '.join(errors)) def validate_modify_parameters(self, body): """ Only the following keys can be modified: enabled, ip, location, name, service_policy """ bad_keys = [key for key in body if key not in ['enabled', 'ip', 'location', 'name', 'service_policy', 'dns_zone', 'ddns_enabled', 'subnet.name', 'fail_if_subnet_conflicts']] if bad_keys: plural = 's' if len(bad_keys) > 1 else '' self.module.fail_json(msg='The following option%s cannot be modified: %s' % (plural, ', '.join(bad_keys))) def build_rest_body(self, modify=None): required_keys = set(['interface_type']) # python 2.6 syntax # running validation twice, as interface_type dictates the second set of requirements self.validate_required_parameters(required_keys) self.validate_rest_input_parameters(action='modify' if modify else 'create') if modify: # force the value of fail_if_subnet_conflicts as it is writeOnly if self.parameters.get('fail_if_subnet_conflicts') is not None: modify['fail_if_subnet_conflicts'] = self.parameters['fail_if_subnet_conflicts'] else: required_keys = set() required_keys.add('interface_name') if self.parameters['interface_type'] == 'fc': self.derive_fc_data_protocol() required_keys.add('data_protocol') if 'home_port' not in self.parameters: # home_port is not supported with 9.7 if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 8, 0): required_keys.add('home_port') else: required_keys.add('current_port') if self.parameters['interface_type'] == 'ip': if 'subnet_name' not in self.parameters: required_keys.add('address') required_keys.add('netmask') required_keys.add('broadcast_domain_home_port_or_home_node') self.validate_required_parameters(required_keys) body, migrate_body, errors = self.set_options_rest(modify) self.fix_errors(body, errors) if errors: self.module.fail_json(msg='Error %s interface, unsupported options: %s' % ('modifying' if modify else 'creating', str(errors))) if modify: self.validate_modify_parameters(body) return body, migrate_body def create_interface_rest(self, body): ''' calling REST to create interface ''' query = {'return_records': 'true'} records, error = rest_generic.post_async(self.rest_api, self.get_net_int_api(), body, query) if error: self.module.fail_json(msg='Error creating interface %s: %s' % (self.parameters['interface_name'], to_native(error)), exception=traceback.format_exc()) return records def create_interface(self, body): ''' calling zapi to create interface ''' if self.use_rest: return self.create_interface_rest(body) required_keys = set(['role', 'home_port']) if self.parameters.get('subnet_name') is None and self.parameters.get('is_ipv4_link_local') is None: required_keys.add('address') required_keys.add('netmask') if self.parameters.get('service_policy') is not None: required_keys.remove('role') data_protocols_obj = self.set_protocol_option(required_keys) self.validate_required_parameters(required_keys) options = {'interface-name': self.parameters['interface_name'], 'vserver': self.parameters['vserver']} NetAppOntapInterface.set_options(options, self.parameters) interface_create = netapp_utils.zapi.NaElement.create_node_with_children('net-interface-create', **options) if data_protocols_obj is not None: interface_create.add_child_elem(data_protocols_obj) try: self.server.invoke_successfully(interface_create, enable_tunneling=True) except netapp_utils.zapi.NaApiError as exc: # msg: "Error Creating interface ansible_interface: NetApp API failed. Reason - 17:A LIF with the same name already exists" if to_native(exc.code) == "17": self.na_helper.changed = False else: self.module.fail_json(msg='Error Creating interface %s: %s' % (self.parameters['interface_name'], to_native(exc)), exception=traceback.format_exc()) def delete_interface_rest(self, uuid): ''' calling zapi to delete interface ''' dummy, error = rest_generic.delete_async(self.rest_api, self.get_net_int_api(), uuid) if error: self.module.fail_json(msg='Error deleting interface %s: %s' % (self.parameters['interface_name'], to_native(error)), exception=traceback.format_exc()) def delete_interface(self, current_status, current_interface, uuid): ''' calling zapi to delete interface ''' if current_status == 'up': self.parameters['admin_status'] = 'down' if self.use_rest: # only for fc interfaces disable is required before delete. if current_interface == 'fc': self.modify_interface_rest(uuid, {'enabled': False}) else: self.modify_interface({'admin_status': 'down'}) if self.use_rest: return self.delete_interface_rest(uuid) interface_delete = netapp_utils.zapi.NaElement.create_node_with_children( 'net-interface-delete', **{'interface-name': self.parameters['interface_name'], 'vserver': self.parameters['vserver']}) try: self.server.invoke_successfully(interface_delete, enable_tunneling=True) except netapp_utils.zapi.NaApiError as exc: self.module.fail_json(msg='Error deleting interface %s: %s' % (self.parameters['interface_name'], to_native(exc)), exception=traceback.format_exc()) def modify_interface_rest(self, uuid, body): ''' calling REST to modify interface ''' if not body: return dummy, error = rest_generic.patch_async(self.rest_api, self.get_net_int_api(), uuid, body) if error: self.module.fail_json(msg='Error modifying interface %s: %s' % (self.parameters['interface_name'], to_native(error)), exception=traceback.format_exc()) def migrate_interface_rest(self, uuid, body): # curiously, we sometimes need to send the request twice (well, always in my experience) errors = [] desired_node = self.na_helper.safe_get(body, ['location', 'node', 'name']) desired_port = self.na_helper.safe_get(body, ['location', 'port', 'name']) for __ in range(12): self.modify_interface_rest(uuid, body) time.sleep(10) node, port, error = self.get_node_port(uuid) if error is None and desired_node in [None, node] and desired_port in [None, port]: return if errors or error is not None: errors.append(str(error)) if errors: self.module.fail_json(msg='Errors waiting for migration to complete: %s' % ' - '.join(errors)) else: self.module.warn('Failed to confirm interface is migrated after 120 seconds') def modify_interface(self, modify, uuid=None, body=None): """ Modify the interface. """ if self.use_rest: return self.modify_interface_rest(uuid, body) # Current_node and current_port don't exist in modify only migrate, so we need to remove them from the list migrate = {} modify_options = dict(modify) if modify_options.get('current_node') is not None: migrate['current_node'] = modify_options.pop('current_node') if modify_options.get('current_port') is not None: migrate['current_port'] = modify_options.pop('current_port') if modify_options: options = {'interface-name': self.parameters['interface_name'], 'vserver': self.parameters['vserver'] } NetAppOntapInterface.set_options(options, modify_options) interface_modify = netapp_utils.zapi.NaElement.create_node_with_children('net-interface-modify', **options) try: self.server.invoke_successfully(interface_modify, enable_tunneling=True) except netapp_utils.zapi.NaApiError as err: self.module.fail_json(msg='Error modifying interface %s: %s' % (self.parameters['interface_name'], to_native(err)), exception=traceback.format_exc()) # if home node has been changed we need to migrate the interface if migrate: self.migrate_interface() def migrate_interface(self): # ZAPI interface_migrate = netapp_utils.zapi.NaElement('net-interface-migrate') if self.parameters.get('current_node') is None: self.module.fail_json(msg='current_node must be set to migrate') interface_migrate.add_new_child('destination-node', self.parameters['current_node']) if self.parameters.get('current_port') is not None: interface_migrate.add_new_child('destination-port', self.parameters['current_port']) interface_migrate.add_new_child('lif', self.parameters['interface_name']) interface_migrate.add_new_child('vserver', self.parameters['vserver']) try: self.server.invoke_successfully(interface_migrate, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error migrating %s: %s' % (self.parameters['current_node'], to_native(error)), exception=traceback.format_exc()) # like with REST, the migration may not be completed on the first try! # just blindly do it twice. try: self.server.invoke_successfully(interface_migrate, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error migrating %s: %s' % (self.parameters['current_node'], to_native(error)), exception=traceback.format_exc()) def rename_interface(self): options = { 'interface-name': self.parameters['from_name'], 'new-name': self.parameters['interface_name'], 'vserver': self.parameters['vserver'] } interface_rename = netapp_utils.zapi.NaElement.create_node_with_children('net-interface-rename', **options) try: self.server.invoke_successfully(interface_rename, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error renaming %s to %s: %s' % (self.parameters['from_name'], self.parameters['interface_name'], to_native(error)), exception=traceback.format_exc()) def get_action(self): modify, rename, new_name = None, None, None current = self.get_interface() cd_action = self.na_helper.get_cd_action(current, self.parameters) if cd_action == 'create' and self.parameters.get('from_name'): # create by renaming existing interface # self.parameters['interface_name'] may be overriden in self.get_interface so save a copy new_name = self.parameters['interface_name'] old_interface = self.get_interface(self.parameters['from_name']) rename = self.na_helper.is_rename_action(old_interface, current) if rename is None: self.module.fail_json(msg='Error renaming interface %s: no interface with from_name %s.' % (self.parameters['interface_name'], self.parameters['from_name'])) if rename: current = old_interface cd_action = None if cd_action is None: modify = self.na_helper.get_modified_attributes(current, self.parameters) if rename and self.use_rest: rename = False if 'interface_name' not in modify: modify['interface_name'] = new_name if modify and modify.get('home_node') == 'localhost': modify.pop('home_node') if not modify: self.na_helper.changed = False return cd_action, modify, rename, current def build_rest_payloads(self, cd_action, modify, current): body, migrate_body = None, None uuid = current.get('uuid') if current else None if self.use_rest: if cd_action == 'create': body, migrate_body = self.build_rest_body() elif modify: # fc interface supports only home_port and port in POST/PATCH. # add home_port and current_port in modify for home_node and current_node respectively to form home_port/port. if modify.get('home_node') and not modify.get('home_port') and self.parameters['interface_type'] == 'fc': modify['home_port'] = current['home_port'] # above will modify home_node of fc interface, after modify if requires to update current_node, it will error out for fc interface. # migrate not supported for fc interface. if modify.get('current_node') and not modify.get('current_port') and self.parameters['interface_type'] == 'fc': modify['current_port'] = current['current_port'] body, migrate_body = self.build_rest_body(modify) if (modify or cd_action == 'delete') and uuid is None: self.module.fail_json(msg='Error, expecting uuid in existing record') desired_home_port = self.na_helper.safe_get(body, ['location', 'home_port']) desired_current_port = self.na_helper.safe_get(migrate_body, ['location', 'port']) # if try to modify both home_port and current_port in FC interface and if its equal, make migrate_body None if self.parameters.get('interface_type') == 'fc' and desired_home_port and desired_current_port and desired_home_port == desired_current_port: migrate_body = None return uuid, body, migrate_body def apply(self): ''' calling all interface features ''' cd_action, modify, rename, current = self.get_action() # build the payloads even in check_mode, to perform validations uuid, body, migrate_body = self.build_rest_payloads(cd_action, modify, current) if self.na_helper.changed and not self.module.check_mode: if rename and not self.use_rest: self.rename_interface() modify.pop('interface_name') if cd_action == 'create': records = self.create_interface(body) if records: # needed for migrate after creation uuid = records['records'][0]['uuid'] elif cd_action == 'delete': # interface type returned in REST but not in ZAPI. interface_type = current['interface_type'] if self.use_rest else None self.delete_interface(current['admin_status'], interface_type, uuid) elif modify: self.modify_interface(modify, uuid, body) if migrate_body: # for 9.7 or earlier, allow modify current node/port for fc interface. if self.parameters.get('interface_type') == 'fc' and self.use_rest and self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 8, 0): self.module.fail_json(msg="Error: cannot migrate FC interface") self.migrate_interface_rest(uuid, migrate_body) result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify) self.module.exit_json(**result) def main(): interface = NetAppOntapInterface() interface.apply() if __name__ == '__main__': main()