From acbffce0796ff8e28ac5646ed8b3fd4e19232223 Mon Sep 17 00:00:00 2001 From: Nathan Swartz Date: Tue, 6 Aug 2019 14:01:22 -0500 Subject: [PATCH] Host dev upstream (#57086) * Improve netapp_e_host module Add host type strings for windows, windows cluster, linux and vmware to netapp_e_host module Make host port information case-insensitive in netapp_e_host module Fix port removal and default group. Fix port reassignment in netapp_e_host module. Fix port label or address change within existing host object in module netapp_e_host Add unit and integration tests * Created new unit test for netapp_e_host module --- .../modules/storage/netapp/netapp_e_host.py | 386 ++++++------ .../targets/netapp_eseries_host/aliases | 2 +- .../targets/netapp_eseries_host/tasks/run.yml | 46 +- .../storage/netapp/test_netapp_e_host.py | 574 +++++++++++++----- 4 files changed, 639 insertions(+), 369 deletions(-) diff --git a/lib/ansible/modules/storage/netapp/netapp_e_host.py b/lib/ansible/modules/storage/netapp/netapp_e_host.py index 30aa65779b..7704e60fbb 100644 --- a/lib/ansible/modules/storage/netapp/netapp_e_host.py +++ b/lib/ansible/modules/storage/netapp/netapp_e_host.py @@ -17,7 +17,9 @@ module: netapp_e_host short_description: NetApp E-Series manage eseries hosts description: Create, update, remove hosts on NetApp E-series storage arrays version_added: '2.2' -author: Kevin Hulquest (@hulquest) +author: + - Kevin Hulquest (@hulquest) + - Nathan Swartz (@ndswartz) extends_documentation_fragment: - netapp.eseries options: @@ -37,13 +39,15 @@ options: - present default: present version_added: 2.7 - host_type_index: + host_type: description: - - The index that maps to host type you wish to create. It is recommended to use the M(netapp_e_facts) module to gather this information. - Alternatively you can use the WSP portal to retrieve the information. + - This is the type of host to be mapped - Required when C(state=present) + - Either one of the following names can be specified, Linux DM-MP, VMWare, Windows, Windows Clustered, or a + host type index which can be found in M(netapp_e_facts) + type: str aliases: - - host_type + - host_type_index ports: description: - A list of host ports you wish to associate with the host. @@ -88,7 +92,6 @@ options: - A local path to a file to be used for debug logging required: False version_added: 2.7 - """ EXAMPLES = """ @@ -96,11 +99,11 @@ EXAMPLES = """ netapp_e_host: ssid: "1" api_url: "10.113.1.101:8443" - api_username: "admin" - api_password: "myPassword" + api_username: admin + api_password: myPassword name: "Host1" state: present - host_type_index: 28 + host_type_index: Linux DM-MP ports: - type: 'iscsi' label: 'PORT_1' @@ -116,11 +119,10 @@ EXAMPLES = """ netapp_e_host: ssid: "1" api_url: "10.113.1.101:8443" - api_username: "admin" - api_password: "myPassword" + api_username: admin + api_password: myPassword name: "Host2" state: absent - """ RETURN = """ @@ -153,10 +155,10 @@ api_url: type: str sample: https://webservices.example.com:8443 version_added: "2.6" - """ import json import logging +import re from pprint import pformat from ansible.module_utils.basic import AnsibleModule @@ -170,6 +172,8 @@ HEADERS = { class Host(object): + HOST_TYPE_INDEXES = {"linux dm-mp": 28, "vmware": 10, "windows": 1, "windows clustered": 8} + def __init__(self): argument_spec = eseries_host_argument_spec() argument_spec.update(dict( @@ -178,57 +182,70 @@ class Host(object): ports=dict(type='list', required=False), force_port=dict(type='bool', default=False), name=dict(type='str', required=True, aliases=['label']), - host_type_index=dict(type='int', aliases=['host_type']), + host_type_index=dict(type='str', aliases=['host_type']), log_path=dict(type='str', required=False), )) - required_if = [ - ["state", "absent", ["name"]], - ["state", "present", ["name", "host_type"]] - ] - - self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if) + self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) self.check_mode = self.module.check_mode args = self.module.params self.group = args['group'] self.ports = args['ports'] self.force_port = args['force_port'] self.name = args['name'] - self.host_type_index = args['host_type_index'] self.state = args['state'] self.ssid = args['ssid'] self.url = args['api_url'] self.user = args['api_username'] self.pwd = args['api_password'] self.certs = args['validate_certs'] - self.post_body = dict() + self.post_body = dict() self.all_hosts = list() + self.host_obj = dict() self.newPorts = list() self.portsForUpdate = list() - self.force_port_update = False + self.portsForRemoval = list() - log_path = args['log_path'] + # Update host type with the corresponding index + host_type = args['host_type_index'] + if host_type: + host_type = host_type.lower() + if host_type in [key.lower() for key in list(self.HOST_TYPE_INDEXES.keys())]: + self.host_type_index = self.HOST_TYPE_INDEXES[host_type] + elif host_type.isdigit(): + self.host_type_index = int(args['host_type_index']) + else: + self.module.fail_json(msg="host_type must be either a host type name or host type index found integer" + " the documentation.") # logging setup self._logger = logging.getLogger(self.__class__.__name__) - - if log_path: + if args['log_path']: logging.basicConfig( - level=logging.DEBUG, filename=log_path, filemode='w', + level=logging.DEBUG, filename=args['log_path'], filemode='w', format='%(relativeCreated)dms %(levelname)s %(module)s.%(funcName)s:%(lineno)d\n %(message)s') if not self.url.endswith('/'): self.url += '/' + # Ensure when state==present then host_type_index is defined + if self.state == "present" and self.host_type_index is None: + self.module.fail_json(msg="Host_type_index is required when state=='present'. Array Id: [%s]" % self.ssid) + # Fix port representation if they are provided with colons if self.ports is not None: for port in self.ports: - if port['type'] != 'iscsi': - port['port'] = port['port'].replace(':', '') + port['label'] = port['label'].lower() + port['type'] = port['type'].lower() + port['port'] = port['port'].lower() + + # Determine whether address is 16-byte WWPN and, if so, remove + if re.match(r'^(0x)?[0-9a-f]{16}$', port['port'].replace(':', '')): + port['port'] = port['port'].replace(':', '').replace('0x', '') - @property def valid_host_type(self): + host_types = None try: (rc, host_types) = request(self.url + 'storage-systems/%s/host-types' % self.ssid, url_password=self.pwd, url_username=self.user, validate_certs=self.certs, headers=HEADERS) @@ -242,22 +259,65 @@ class Host(object): except IndexError: self.module.fail_json(msg="There is no host type with index %s" % self.host_type_index) - @property - def host_ports_available(self): - """Determine if the hostPorts requested have already been assigned""" + def assigned_host_ports(self, apply_unassigning=False): + """Determine if the hostPorts requested have already been assigned and return list of required used ports.""" + used_host_ports = {} for host in self.all_hosts: if host['label'] != self.name: for host_port in host['hostSidePorts']: for port in self.ports: - if (port['port'] == host_port['address'] or port['label'] == host_port['label']): + if port['port'] == host_port["address"] or port['label'] == host_port['label']: if not self.force_port: - self.module.fail_json( - msg="There are no host ports available OR there are not enough unassigned host ports") + self.module.fail_json(msg="There are no host ports available OR there are not enough" + " unassigned host ports") else: - return False - return True + # Determine port reference + port_ref = [port["hostPortRef"] for port in host["ports"] + if port["hostPortName"] == host_port["address"]] + port_ref.extend([port["initiatorRef"] for port in host["initiators"] + if port["nodeName"]["iscsiNodeName"] == host_port["address"]]) + + # Create dictionary of hosts containing list of port references + if host["hostRef"] not in used_host_ports.keys(): + used_host_ports.update({host["hostRef"]: port_ref}) + else: + used_host_ports[host["hostRef"]].extend(port_ref) + else: + for host_port in host['hostSidePorts']: + for port in self.ports: + if ((host_port['label'] == port['label'] and host_port['address'] != port['port']) or + (host_port['label'] != port['label'] and host_port['address'] == port['port'])): + if not self.force_port: + self.module.fail_json(msg="There are no host ports available OR there are not enough" + " unassigned host ports") + else: + # Determine port reference + port_ref = [port["hostPortRef"] for port in host["ports"] + if port["hostPortName"] == host_port["address"]] + port_ref.extend([port["initiatorRef"] for port in host["initiators"] + if port["nodeName"]["iscsiNodeName"] == host_port["address"]]) + + # Create dictionary of hosts containing list of port references + if host["hostRef"] not in used_host_ports.keys(): + used_host_ports.update({host["hostRef"]: port_ref}) + else: + used_host_ports[host["hostRef"]].extend(port_ref) + + # Unassign assigned ports + if apply_unassigning: + for host_ref in used_host_ports.keys(): + try: + rc, resp = request(self.url + 'storage-systems/%s/hosts/%s' % (self.ssid, host_ref), + url_username=self.user, url_password=self.pwd, headers=HEADERS, + validate_certs=self.certs, method='POST', + data=json.dumps({"portsToRemove": used_host_ports[host_ref]})) + except Exception as err: + self.module.fail_json(msg="Failed to unassign host port. Host Id [%s]. Array Id [%s]. Ports [%s]." + " Error [%s]." % (self.host_obj['id'], self.ssid, + used_host_ports[host_ref], to_native(err))) + + return used_host_ports - @property def group_id(self): if self.group: try: @@ -277,12 +337,13 @@ class Host(object): # Return the value equivalent of no group return "0000000000000000000000000000000000000000" - @property def host_exists(self): """Determine if the requested host exists As a side effect, set the full list of defined hosts in 'all_hosts', and the target host in 'host_obj'. """ + match = False all_hosts = list() + try: (rc, all_hosts) = request(self.url + 'storage-systems/%s/hosts' % self.ssid, url_password=self.pwd, url_username=self.user, validate_certs=self.certs, headers=HEADERS) @@ -290,104 +351,65 @@ class Host(object): self.module.fail_json( msg="Failed to determine host existence. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err))) - self.all_hosts = all_hosts - # Augment the host objects for host in all_hosts: + for port in host['hostSidePorts']: + port['type'] = port['type'].lower() + port['address'] = port['address'].lower() + port['label'] = port['label'].lower() + # Augment hostSidePorts with their ID (this is an omission in the API) - host_side_ports = host['hostSidePorts'] - initiators = dict((port['label'], port['id']) for port in host['initiators']) ports = dict((port['label'], port['id']) for port in host['ports']) - ports.update(initiators) - for port in host_side_ports: - if port['label'] in ports: - port['id'] = ports[port['label']] + ports.update((port['label'], port['id']) for port in host['initiators']) - try: # Try to grab the host object - self.host_obj = list(filter(lambda host: host['label'] == self.name, all_hosts))[0] - return True - except IndexError: - # Host with the name passed in does not exist - return False + for host_side_port in host['hostSidePorts']: + if host_side_port['label'] in ports: + host_side_port['id'] = ports[host_side_port['label']] + + if host['label'] == self.name: + self.host_obj = host + match = True + + self.all_hosts = all_hosts + return match - @property def needs_update(self): """Determine whether we need to update the Host object As a side effect, we will set the ports that we need to update (portsForUpdate), and the ports we need to add (newPorts), on self. - :return: """ - needs_update = False - - if self.host_obj['clusterRef'] != self.group_id or self.host_obj['hostTypeIndex'] != self.host_type_index: + changed = False + if (self.host_obj["clusterRef"].lower() != self.group_id().lower() or + self.host_obj["hostTypeIndex"] != self.host_type_index): self._logger.info("Either hostType or the clusterRef doesn't match, an update is required.") - needs_update = True + changed = True + current_host_ports = dict((port["id"], {"type": port["type"], "port": port["address"], "label": port["label"]}) + for port in self.host_obj["hostSidePorts"]) if self.ports: - self._logger.debug("Determining if ports need to be updated.") - # Find the names of all defined ports - port_names = set(port['label'] for port in self.host_obj['hostSidePorts']) - port_addresses = set(port['address'] for port in self.host_obj['hostSidePorts']) + for port in self.ports: + for current_host_port_id in current_host_ports.keys(): + if port == current_host_ports[current_host_port_id]: + current_host_ports.pop(current_host_port_id) + break + elif port["port"] == current_host_ports[current_host_port_id]["port"]: + if self.port_on_diff_host(port) and not self.force_port: + self.module.fail_json(msg="The port you specified [%s] is associated with a different host." + " Specify force_port as True or try a different port spec" % port) - # If we have ports defined and there are no ports on the host object, we need an update - if not self.host_obj['hostSidePorts']: - needs_update = True - for arg_port in self.ports: - # First a quick check to see if the port is mapped to a different host - if not self.port_on_diff_host(arg_port): - # The port (as defined), currently does not exist - if arg_port['label'] not in port_names: - needs_update = True - # This port has not been defined on the host at all - if arg_port['port'] not in port_addresses: - self.newPorts.append(arg_port) - # A port label update has been requested - else: - self.portsForUpdate.append(arg_port) - # The port does exist, does it need to be updated? - else: - for obj_port in self.host_obj['hostSidePorts']: - if arg_port['label'] == obj_port['label']: - # Confirmed that port arg passed in exists on the host - # port_id = self.get_port_id(obj_port['label']) - if arg_port['type'] != obj_port['type']: - needs_update = True - self.portsForUpdate.append(arg_port) - if 'iscsiChapSecret' in arg_port: - # No way to know the current secret attr, so always return True just in case - needs_update = True - self.portsForUpdate.append(arg_port) + if (port["label"] != current_host_ports[current_host_port_id]["label"] or + port["type"] != current_host_ports[current_host_port_id]["type"]): + current_host_ports.pop(current_host_port_id) + self.portsForUpdate.append({"portRef": current_host_port_id, "port": port["port"], + "label": port["label"], "hostRef": self.host_obj["hostRef"]}) + break else: - # If the user wants the ports to be reassigned, do it - if self.force_port: - self.force_port_update = True - needs_update = True - else: - self.module.fail_json( - msg="The port you specified:\n%s\n is associated with a different host. Specify force_port" - " as True or try a different port spec" % arg_port - ) - self._logger.debug("Is an update required ?=%s", needs_update) - return needs_update + self.newPorts.append(port) - def get_ports_on_host(self): - """Retrieve the hostPorts that are defined on the target host - :return: a list of hostPorts with their labels and ids - Example: - [ - { - 'name': 'hostPort1', - 'id': '0000000000000000000000' - } - ] - """ - ret = dict() - for host in self.all_hosts: - if host['name'] == self.name: - ports = host['hostSidePorts'] - for port in ports: - ret[port['address']] = {'label': port['label'], 'id': port['id'], 'address': port['address']} - return ret + self.portsForRemoval = list(current_host_ports.keys()) + changed = any([self.newPorts, self.portsForUpdate, self.portsForRemoval, changed]) + + return changed def port_on_diff_host(self, arg_port): """ Checks to see if a passed in port arg is present on a different host """ @@ -401,68 +423,24 @@ class Host(object): return True return False - def get_port(self, label, address): - for host in self.all_hosts: - for port in host['hostSidePorts']: - if port['label'] == label or port['address'] == address: - return port - - def reassign_ports(self, apply=True): - post_body = dict( - portsToUpdate=dict() - ) - - for port in self.ports: - if self.port_on_diff_host(port): - host_port = self.get_port(port['label'], port['port']) - post_body['portsToUpdate'].update(dict( - portRef=host_port['id'], - hostRef=self.host_obj['id'], - label=port['label'] - # Doesn't yet address port identifier or chap secret - )) - - self._logger.info("reassign_ports: %s", pformat(post_body)) - - if apply: - try: - (rc, self.host_obj) = request( - self.url + 'storage-systems/%s/hosts/%s' % (self.ssid, self.host_obj['id']), - url_username=self.user, url_password=self.pwd, headers=HEADERS, - validate_certs=self.certs, method='POST', data=json.dumps(post_body)) - except Exception as err: - self.module.fail_json( - msg="Failed to reassign host port. Host Id [%s]. Array Id [%s]. Error [%s]." % ( - self.host_obj['id'], self.ssid, to_native(err))) - - return post_body - def update_host(self): - self._logger.debug("Beginning the update for host=%s.", self.name) + self._logger.info("Beginning the update for host=%s.", self.name) if self.ports: + + # Remove ports that need reassigning from their current host. + self.assigned_host_ports(apply_unassigning=True) + + self.post_body["portsToUpdate"] = self.portsForUpdate + self.post_body["ports"] = self.newPorts self._logger.info("Requested ports: %s", pformat(self.ports)) - if self.host_ports_available or self.force_port: - self.reassign_ports(apply=True) - # Make sure that only ports that aren't being reassigned are passed into the ports attr - host_ports = self.get_ports_on_host() - ports_for_update = list() - self._logger.info("Ports on host: %s", pformat(host_ports)) - for port in self.portsForUpdate: - if port['port'] in host_ports: - defined_port = host_ports.get(port['port']) - defined_port.update(port) - defined_port['portRef'] = defined_port['id'] - ports_for_update.append(defined_port) - self._logger.info("Ports to update: %s", pformat(ports_for_update)) - self._logger.info("Ports to define: %s", pformat(self.newPorts)) - self.post_body['portsToUpdate'] = ports_for_update - self.post_body['ports'] = self.newPorts else: - self._logger.debug("No host ports were defined.") + self._logger.info("No host ports were defined.") if self.group: - self.post_body['groupId'] = self.group_id + self.post_body['groupId'] = self.group_id() + else: + self.post_body['groupId'] = "0000000000000000000000000000000000000000" self.post_body['hostType'] = dict(index=self.host_type_index) @@ -482,46 +460,36 @@ class Host(object): def create_host(self): self._logger.info("Creating host definition.") - needs_reassignment = False + + # Remove ports that need reassigning from their current host. + self.assigned_host_ports(apply_unassigning=True) + + # needs_reassignment = False post_body = dict( name=self.name, hostType=dict(index=self.host_type_index), - groupId=self.group_id, + groupId=self.group_id(), ) + if self.ports: - # Check that all supplied port args are valid - if self.host_ports_available: - self._logger.info("The host-ports requested are available.") - post_body.update(ports=self.ports) - elif not self.force_port: - self.module.fail_json( - msg="You supplied ports that are already in use." - " Supply force_port to True if you wish to reassign the ports") - else: - needs_reassignment = True + post_body.update(ports=self.ports) api = self.url + "storage-systems/%s/hosts" % self.ssid self._logger.info('POST => url=%s, body=%s', api, pformat(post_body)) - if not (self.host_exists and self.check_mode): - try: - (rc, self.host_obj) = request(api, method='POST', - url_username=self.user, url_password=self.pwd, validate_certs=self.certs, - data=json.dumps(post_body), headers=HEADERS) - except Exception as err: - self.module.fail_json( - msg="Failed to create host. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err))) - else: - payload = self.build_success_payload(self.host_obj) - self.module.exit_json(changed=False, - msg="Host already exists. Id [%s]. Host [%s]." % (self.ssid, self.name), **payload) - - self._logger.info("Created host, beginning port re-assignment.") - if needs_reassignment: - self.reassign_ports() + if not self.check_mode: + if not self.host_exists(): + try: + (rc, self.host_obj) = request(api, method='POST', url_username=self.user, url_password=self.pwd, validate_certs=self.certs, + data=json.dumps(post_body), headers=HEADERS) + except Exception as err: + self.module.fail_json( + msg="Failed to create host. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err))) + else: + payload = self.build_success_payload(self.host_obj) + self.module.exit_json(changed=False, msg="Host already exists. Id [%s]. Host [%s]." % (self.ssid, self.name), **payload) payload = self.build_success_payload(self.host_obj) - self.module.exit_json(changed=True, msg='Host created.', **payload) def remove_host(self): @@ -547,17 +515,17 @@ class Host(object): def apply(self): if self.state == 'present': - if self.host_exists: - if self.needs_update and self.valid_host_type: + if self.host_exists(): + if self.needs_update() and self.valid_host_type(): self.update_host() else: payload = self.build_success_payload(self.host_obj) self.module.exit_json(changed=False, msg="Host already present; no changes required.", **payload) - elif self.valid_host_type: + elif self.valid_host_type(): self.create_host() else: payload = self.build_success_payload() - if self.host_exists: + if self.host_exists(): self.remove_host() self.module.exit_json(changed=True, msg="Host removed.", **payload) else: diff --git a/test/integration/targets/netapp_eseries_host/aliases b/test/integration/targets/netapp_eseries_host/aliases index d314d14a74..28cd22b2e7 100644 --- a/test/integration/targets/netapp_eseries_host/aliases +++ b/test/integration/targets/netapp_eseries_host/aliases @@ -1,7 +1,7 @@ # This test is not enabled by default, but can be utilized by defining required variables in integration_config.yml # Example integration_config.yml: # --- -#netapp_e_api_host: 10.113.1.111:8443 +#netapp_e_api_host: 192.168.1.1 #netapp_e_api_username: admin #netapp_e_api_password: myPass #netapp_e_ssid: 1 diff --git a/test/integration/targets/netapp_eseries_host/tasks/run.yml b/test/integration/targets/netapp_eseries_host/tasks/run.yml index 70519b4b94..1fd5c65a17 100644 --- a/test/integration/targets/netapp_eseries_host/tasks/run.yml +++ b/test/integration/targets/netapp_eseries_host/tasks/run.yml @@ -22,38 +22,38 @@ update_host_type: 28 ports: - type: 'iscsi' - label: 'I_1' - port: 'iqn.1996-04.de.suse:01:56f86f9bd1fe' + label: 'I1_1' + port: 'iqn.1996-04.de.suse:01:56f86f9bd1fe-PORT1' - type: 'iscsi' - label: 'I_2' - port: 'iqn.1996-04.de.suse:01:56f86f9bd1ff' + label: 'I1_2' + port: 'iqn.1996-04.de.suse:01:56f86f9bd1ff-port1' ports2: - type: 'iscsi' - label: 'I_1' - port: 'iqn.1996-04.de.suse:01:56f86f9bd1fe' + label: 'I1_1' + port: 'iqn.1996-04.de.suse:01:56f86f9bd1fe-port2' - type: 'iscsi' - label: 'I_2' - port: 'iqn.1996-04.de.suse:01:56f86f9bd1ff' - - type: 'fc' - label: 'FC_3' - port: '10:00:8C:7C:FF:1A:B9:01' + label: 'I1_2' + port: 'iqn.1996-04.de.suse:01:56f86f9bd1ff-port2' + - type: 'iscsi' + label: 'I1_3' + port: 'iqn.1996-04.redhat:01:56f86f9bd1fe-PORT1' 2: host_type: 27 update_host_type: 28 ports: - - type: 'fc' - label: 'FC_1' - port: '10:00:8C:7C:FF:1A:B9:01' - - type: 'fc' - label: 'FC_2' - port: '10:00:8C:7C:FF:1A:B9:00' + - type: 'iscsi' + label: 'I2_1' + port: 'iqn.1996-04.redhat:01:56f86f9bd1fe-port1' + - type: 'iscsi' + label: 'I2_2' + port: 'iqn.1996-04.redhat:01:56f86f9bd1ff-port1' ports2: - - type: 'fc' - label: 'FC_6' - port: '10:00:8C:7C:FF:1A:B9:01' - - type: 'fc' - label: 'FC_4' - port: '10:00:8C:7C:FF:1A:B9:00' + - type: 'iscsi' + label: 'I2_1' + port: 'iqn.1996-04.redhat:01:56f86f9bd1fe-port2' + - type: 'iscsi' + label: 'I2_2' + port: 'iqn.1996-04.redhat:01:56f86f9bd1ff-PORT2' # ******************************************** diff --git a/test/units/modules/storage/netapp/test_netapp_e_host.py b/test/units/modules/storage/netapp/test_netapp_e_host.py index 4646805b46..7e0f7e2977 100644 --- a/test/units/modules/storage/netapp/test_netapp_e_host.py +++ b/test/units/modules/storage/netapp/test_netapp_e_host.py @@ -1,17 +1,15 @@ # (c) 2018, NetApp Inc. # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -from mock import MagicMock - -from ansible.module_utils import netapp -from ansible.modules.storage.netapp import netapp_e_host from ansible.modules.storage.netapp.netapp_e_host import Host from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args __metaclass__ = type -import mock -from units.compat.mock import patch -from ansible.module_utils._text import to_bytes + +try: + from unittest import mock +except ImportError: + import mock class HostTest(ModuleTestCase): @@ -24,6 +22,7 @@ class HostTest(ModuleTestCase): } HOST = { 'name': '1', + 'hostRef': '123', 'label': '1', 'id': '0' * 30, 'clusterRef': 40 * '0', @@ -41,6 +40,48 @@ class HostTest(ModuleTestCase): 'initiators': [], 'ports': [], } + EXISTING_HOSTS = [ + {"hostRef": "84000000600A098000A4B28D00303D065D430118", "clusterRef": "0000000000000000000000000000000000000000", "label": "beegfs_storage1", + "hostTypeIndex": 28, "ports": [], "initiators": [{"initiatorRef": "89000000600A098000A4B28D00303CF55D4300E3", + "nodeName": {"ioInterfaceType": "iscsi", + "iscsiNodeName": "iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818", + "remoteNodeWWN": None, "nvmeNodeName": None}, + "alias": {"ioInterfaceType": "iscsi", "iscsiAlias": ""}, "label": "beegfs_storage1_iscsi_0", + "hostRef": "84000000600A098000A4B28D00303D065D430118", + "id": "89000000600A098000A4B28D00303CF55D4300E3"}], + "hostSidePorts": [{"type": "iscsi", "address": "iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818", "label": "beegfs_storage1_iscsi_0"}], + "id": "84000000600A098000A4B28D00303D065D430118", "name": "beegfs_storage1"}, + {"hostRef": "84000000600A098000A4B9D10030370B5D430109", "clusterRef": "0000000000000000000000000000000000000000", "label": "beegfs_metadata1", + "hostTypeIndex": 28, "ports": [], "initiators": [{"initiatorRef": "89000000600A098000A4B28D00303CFC5D4300F7", + "nodeName": {"ioInterfaceType": "iscsi", + "iscsiNodeName": "iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8", + "remoteNodeWWN": None, "nvmeNodeName": None}, + "alias": {"ioInterfaceType": "iscsi", "iscsiAlias": ""}, "label": "beegfs_metadata1_iscsi_0", + "hostRef": "84000000600A098000A4B9D10030370B5D430109", + "id": "89000000600A098000A4B28D00303CFC5D4300F7"}], + "hostSidePorts": [{"type": "iscsi", "address": "iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8", "label": "beegfs_metadata1_iscsi_0"}], + "id": "84000000600A098000A4B9D10030370B5D430109", "name": "beegfs_metadata1"}, + {"hostRef": "84000000600A098000A4B9D10030370B5D430109", "clusterRef": "85000000600A098000A4B9D1003637135D483DEB", "label": "beegfs_metadata2", + "hostTypeIndex": 28, "ports": [], "initiators": [{"initiatorRef": "89000000600A098000A4B28D00303CFC5D4300F7", + "nodeName": {"ioInterfaceType": "iscsi", + "iscsiNodeName": "iqn.used_elsewhere", + "remoteNodeWWN": None, "nvmeNodeName": None}, + "alias": {"ioInterfaceType": "iscsi", "iscsiAlias": ""}, "label": "beegfs_metadata2_iscsi_0", + "hostRef": "84000000600A098000A4B9D10030370B5D430109", + "id": "89000000600A098000A4B28D00303CFC5D4300F7"}], + "hostSidePorts": [{"type": "iscsi", "address": "iqn.used_elsewhere", "label": "beegfs_metadata2_iscsi_0"}], + "id": "84000000600A098000A4B9D10030370B5D430120", "name": "beegfs_metadata2"}] + HOST_GROUPS = [{"clusterRef": "85000000600A098000A4B9D1003637135D483DEB", "label": "test_group", "isSAControlled": False, + "confirmLUNMappingCreation": False, "protectionInformationCapableAccessMethod": True, "isLun0Restricted": False, + "id": "85000000600A098000A4B9D1003637135D483DEB", "name": "test_group"}] + HOST_TYPES = [{"name": "FactoryDefault", "index": 0, "code": "FactoryDefault"}, + {"name": "Windows 2000/Server 2003/Server 2008 Non-Clustered", "index": 1, "code": "W2KNETNCL"}, + {"name": "Solaris", "index": 2, "code": "SOL"}, + {"name": "Linux", "index": 6, "code": "LNX"}, + {"name": "LnxALUA", "index": 7, "code": "LnxALUA"}, + {"name": "Windows 2000/Server 2003/Server 2008 Clustered", "index": 8, "code": "W2KNETCL"}, + {"name": "LnxTPGSALUA_SF", "index": 27, "code": "LnxTPGSALUA_SF"}, + {"name": "LnxDHALUA", "index": 28, "code": "LnxDHALUA"}] REQ_FUNC = 'ansible.modules.storage.netapp.netapp_e_host.request' def _set_args(self, args): @@ -48,140 +89,401 @@ class HostTest(ModuleTestCase): module_args.update(args) set_module_args(module_args) - def test_delete_host(self): - """Validate removing a host object""" - self._set_args({ - 'state': 'absent' - }) - host = Host() - with self.assertRaises(AnsibleExitJson) as result: - # We expect 2 calls to the API, the first to retrieve the host objects defined, - # the second to remove the host definition. - with mock.patch(self.REQ_FUNC, side_effect=[(200, [self.HOST]), (204, {})]) as request: - host.apply() - self.assertEquals(request.call_count, 2) - # We expect the module to make changes - self.assertEquals(result.exception.args[0]['changed'], True) + def test_host_exists_pass(self): + """Verify host_exists produces expected results.""" + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'new_host', 'host_type': 'linux dm-mp', 'force_port': False, + 'ports': [{'label': 'new_host_port_1', 'type': 'fc', 'port': '0x08ef08ef08ef08ef'}]}) + host = Host() + self.assertFalse(host.host_exists()) - def test_delete_host_no_changes(self): - """Ensure that removing a host that doesn't exist works correctly.""" - self._set_args({ - 'state': 'absent' - }) - host = Host() - with self.assertRaises(AnsibleExitJson) as result: - # We expect a single call to the API: retrieve the defined hosts. - with mock.patch(self.REQ_FUNC, return_value=(200, [])): - host.apply() - # We should not mark changed=True - self.assertEquals(result.exception.args[0]['changed'], False) + self._set_args({'state': 'present', 'name': 'does_not_exist', 'host_type': 'linux dm-mp', + 'ports': [{'label': 'beegfs_storage1_iscsi_0', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818'}]}) + host = Host() + self.assertFalse(host.host_exists()) - def test_host_exists(self): - """Test host_exists method""" - self._set_args({ - 'state': 'absent' - }) - host = Host() - with mock.patch(self.REQ_FUNC, return_value=(200, [self.HOST])) as request: - host_exists = host.host_exists - self.assertTrue(host_exists, msg="This host should exist!") + self._set_args({'state': 'present', 'name': 'beegfs_storage1', 'host_type': 'linux dm-mp', + 'ports': [{'label': 'beegfs_storage1_iscsi_0', 'type': 'iscsi', 'port': 'iqn.differentiqn.org'}]}) + host = Host() + self.assertTrue(host.host_exists()) - def test_host_exists_negative(self): - """Test host_exists method with no matching hosts to return""" - self._set_args({ - 'state': 'absent' - }) - host = Host() - with mock.patch(self.REQ_FUNC, return_value=(200, [self.HOST_ALT])) as request: - host_exists = host.host_exists - self.assertFalse(host_exists, msg="This host should exist!") + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': True, + 'ports': [{'label': 'beegfs_metadata1_iscsi_0', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818'}]}) + host = Host() + self.assertTrue(host.host_exists()) def test_host_exists_fail(self): - """Ensure we do not dump a stack trace if we fail to make the request""" - self._set_args({ - 'state': 'absent' - }) + """Verify host_exists produces expected exceptions.""" + self._set_args({'state': 'present', 'host_type': 'linux dm-mp', 'ports': [{'label': 'abc', 'type': 'iscsi', 'port': 'iqn:0'}]}) host = Host() - with self.assertRaises(AnsibleFailJson): - with mock.patch(self.REQ_FUNC, side_effect=Exception("http_error")) as request: - host_exists = host.host_exists + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to determine host existence."): + with mock.patch(self.REQ_FUNC, return_value=Exception()): + host.host_exists() - def test_needs_update_host_type(self): - """Ensure a changed host_type triggers an update""" - self._set_args({ - 'state': 'present', - 'host_type': 27 - }) + def test_needs_update_pass(self): + """Verify needs_update produces expected results.""" + # No changes + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', + 'ports': [{'label': 'beegfs_metadata1_iscsi_0', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + host.host_exists() + self.assertFalse(host.needs_update()) + + # Change host type + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': False, + 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi', 'port': 'iqn.not_used'}]}) + host = Host() + host.host_exists() + self.assertTrue(host.needs_update()) + + # Add port to host + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False, + 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi', 'port': 'iqn.not_used'}]}) + host = Host() + host.host_exists() + self.assertTrue(host.needs_update()) + + # Change port name + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False, + 'ports': [{'label': 'beegfs_metadata1_iscsi_2', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + host.host_exists() + self.assertTrue(host.needs_update()) + + # take port from another host by force + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': True, + 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + host.host_exists() + self.assertTrue(host.needs_update()) + + def test_needs_update_fail(self): + """Verify needs_update produces expected exceptions.""" + with self.assertRaisesRegexp(AnsibleFailJson, "is associated with a different host."): + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False, + 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + host.host_exists() + host.needs_update() + + def test_valid_host_type_pass(self): + """Validate the available host types.""" + with mock.patch(self.REQ_FUNC, return_value=(200, self.HOST_TYPES)): + self._set_args({'state': 'present', 'host_type': '0'}) + host = Host() + self.assertTrue(host.valid_host_type()) + self._set_args({'state': 'present', 'host_type': '28'}) + host = Host() + self.assertTrue(host.valid_host_type()) + self._set_args({'state': 'present', 'host_type': 'windows'}) + host = Host() + self.assertTrue(host.valid_host_type()) + self._set_args({'state': 'present', 'host_type': 'linux dm-mp'}) + host = Host() + self.assertTrue(host.valid_host_type()) + + def test_valid_host_type_fail(self): + """Validate the available host types.""" + with self.assertRaisesRegexp(AnsibleFailJson, "host_type must be either a host type name or host type index found integer the documentation"): + self._set_args({'state': 'present', 'host_type': 'non-host-type'}) + host = Host() + + with mock.patch(self.REQ_FUNC, return_value=(200, self.HOST_TYPES)): + with self.assertRaisesRegexp(AnsibleFailJson, "There is no host type with index"): + self._set_args({'state': 'present', 'host_type': '4'}) + host = Host() + host.valid_host_type() + + with mock.patch(self.REQ_FUNC, return_value=Exception()): + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to get host types."): + self._set_args({'state': 'present', 'host_type': '4'}) + host = Host() + host.valid_host_type() + + def test_group_id_pass(self): + """Verify group_id produces expected results.""" + with mock.patch(self.REQ_FUNC, return_value=(200, self.HOST_GROUPS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False, + 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + self.assertEqual(host.group_id(), "0000000000000000000000000000000000000000") + + self._set_args({'state': 'present', 'name': 'beegfs_metadata2', 'host_type': 'linux dm-mp', 'force_port': False, 'group': 'test_group', + 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + self.assertEqual(host.group_id(), "85000000600A098000A4B9D1003637135D483DEB") + + def test_group_id_fail(self): + """Verify group_id produces expected exceptions.""" + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to get host groups."): + with mock.patch(self.REQ_FUNC, return_value=Exception()): + self._set_args({'state': 'present', 'name': 'beegfs_metadata2', 'host_type': 'linux dm-mp', 'force_port': False, 'group': 'test_group2', + 'ports': [ + {'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + host.group_id() + + with self.assertRaisesRegexp(AnsibleFailJson, "No group with the name:"): + with mock.patch(self.REQ_FUNC, return_value=(200, self.HOST_GROUPS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata2', 'host_type': 'linux dm-mp', 'force_port': False, 'group': 'test_group2', + 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + host.group_id() + + def test_assigned_host_ports_pass(self): + """Verify assigned_host_ports gives expected results.""" + + # Add an unused port to host + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False, + 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi', 'port': 'iqn.not_used'}]}) + host = Host() + host.host_exists() + self.assertTrue(host.needs_update()) + self.assertEquals(host.assigned_host_ports(), {}) + + # Change port name (force) + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': True, + 'ports': [{'label': 'beegfs_metadata1_iscsi_2', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + host.host_exists() + self.assertTrue(host.needs_update()) + self.assertEquals(host.assigned_host_ports(), {'84000000600A098000A4B9D10030370B5D430109': ['89000000600A098000A4B28D00303CFC5D4300F7']}) + + # Change port type + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': True, + 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'fc', 'port': '08:ef:7e:24:52:a0'}]}) + host = Host() + host.host_exists() + self.assertTrue(host.needs_update()) + self.assertEquals(host.assigned_host_ports(), {}) + + # take port from another host by force + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': True, + 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', 'port': 'iqn.used_elsewhere'}]}) + host = Host() + host.host_exists() + self.assertTrue(host.needs_update()) + self.assertEquals(host.assigned_host_ports(), {'84000000600A098000A4B9D10030370B5D430109': ['89000000600A098000A4B28D00303CFC5D4300F7']}) + + # take port from another host by force + with mock.patch(self.REQ_FUNC, side_effect=[(200, self.EXISTING_HOSTS), (200, {})]): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': True, + 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', 'port': 'iqn.used_elsewhere'}]}) + host = Host() + host.host_exists() + self.assertTrue(host.needs_update()) + self.assertEquals(host.assigned_host_ports(apply_unassigning=True), + {'84000000600A098000A4B9D10030370B5D430109': ['89000000600A098000A4B28D00303CFC5D4300F7']}) + + def test_assigned_host_ports_fail(self): + """Verify assigned_host_ports gives expected exceptions.""" + # take port from another + with self.assertRaisesRegexp(AnsibleFailJson, "There are no host ports available OR there are not enough unassigned host ports"): + with mock.patch(self.REQ_FUNC, side_effect=[(200, self.EXISTING_HOSTS)]): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False, + 'ports': [{'label': 'beegfs_metadata1_iscsi_2', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + host.host_exists() + self.assertTrue(host.needs_update()) + host.assigned_host_ports(apply_unassigning=True) + + # take port from another host and fail because force == False + with self.assertRaisesRegexp(AnsibleFailJson, "There are no host ports available OR there are not enough unassigned host ports"): + with mock.patch(self.REQ_FUNC, side_effect=[(200, self.EXISTING_HOSTS)]): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False, + 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', 'port': 'iqn.used_elsewhere'}]}) + host = Host() + host.host_exists() + self.assertTrue(host.needs_update()) + host.assigned_host_ports(apply_unassigning=True) + + # take port from another host and fail because force == False + with self.assertRaisesRegexp(AnsibleFailJson, "There are no host ports available OR there are not enough unassigned host ports"): + with mock.patch(self.REQ_FUNC, side_effect=[(200, self.EXISTING_HOSTS)]): + self._set_args({'state': 'present', 'name': 'beegfs_metadata3', 'host_type': 'linux dm-mp', 'force_port': False, + 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', 'port': 'iqn.used_elsewhere'}]}) + host = Host() + host.host_exists() + host.assigned_host_ports(apply_unassigning=True) + + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to unassign host port."): + with mock.patch(self.REQ_FUNC, side_effect=[(200, self.EXISTING_HOSTS), Exception()]): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': True, + 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', 'port': 'iqn.used_elsewhere'}]}) + host = Host() + host.host_exists() + self.assertTrue(host.needs_update()) + host.assigned_host_ports(apply_unassigning=True) + + def test_update_host_pass(self): + """Verify update_host produces expected results.""" + # Change host type + with self.assertRaises(AnsibleExitJson): + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': True, + 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818'}]}) + host = Host() + host.build_success_payload = lambda x: {} + host.host_exists() + self.assertTrue(host.needs_update()) + host.update_host() + + # Change port iqn + with self.assertRaises(AnsibleExitJson): + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False, + 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi', 'port': 'iqn.not_used'}]}) + host = Host() + host.build_success_payload = lambda x: {} + host.host_exists() + self.assertTrue(host.needs_update()) + host.update_host() + + # Change port type to fc + with self.assertRaises(AnsibleExitJson): + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False, + 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'fc', 'port': '0x08ef08ef08ef08ef'}]}) + host = Host() + host.build_success_payload = lambda x: {} + host.host_exists() + self.assertTrue(host.needs_update()) + host.update_host() + + # Change port name + with self.assertRaises(AnsibleExitJson): + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': True, + 'ports': [{'label': 'beegfs_metadata1_iscsi_12', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + host.build_success_payload = lambda x: {} + host.host_exists() + self.assertTrue(host.needs_update()) + host.update_host() + + # Change group + with self.assertRaises(AnsibleExitJson): + with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': False, 'group': 'test_group', + 'ports': [{'label': 'beegfs_metadata1_iscsi_0', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + host.build_success_payload = lambda x: {} + host.group_id = lambda: "85000000600A098000A4B9D1003637135D483DEB" + host.host_exists() + self.assertTrue(host.needs_update()) + host.update_host() + + def test_update_host_fail(self): + """Verify update_host produces expected exceptions.""" + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to update host."): + with mock.patch(self.REQ_FUNC, side_effect=[(200, self.EXISTING_HOSTS), Exception()]): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': False, 'group': 'test_group', + 'ports': [{'label': 'beegfs_metadata1_iscsi_0', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + host.build_success_payload = lambda x: {} + host.group_id = lambda: "85000000600A098000A4B9D1003637135D483DEB" + host.host_exists() + self.assertTrue(host.needs_update()) + host.update_host() + + def test_create_host_pass(self): + """Verify create_host produces expected results.""" + def _assigned_host_ports(apply_unassigning=False): + return None + + with self.assertRaises(AnsibleExitJson): + with mock.patch(self.REQ_FUNC, return_value=(200, {'id': '84000000600A098000A4B9D10030370B5D430109'})): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': True, 'group': 'test_group', + 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818'}]}) + host = Host() + host.host_exists = lambda: False + host.assigned_host_ports = _assigned_host_ports + host.build_success_payload = lambda x: {} + host.group_id = lambda: "85000000600A098000A4B9D1003637135D483DEB" + host.create_host() + + def test_create_host_fail(self): + """Verify create_host produces expected exceptions.""" + def _assigned_host_ports(apply_unassigning=False): + return None + + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to create host."): + with mock.patch(self.REQ_FUNC, return_value=Exception()): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': True, 'group': 'test_group', + 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818'}]}) + host = Host() + host.host_exists = lambda: False + host.assigned_host_ports = _assigned_host_ports + host.build_success_payload = lambda x: {} + host.group_id = lambda: "85000000600A098000A4B9D1003637135D483DEB" + host.create_host() + + with self.assertRaisesRegexp(AnsibleExitJson, "Host already exists."): + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': True, 'group': 'test_group', + 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818'}]}) + host = Host() + host.host_exists = lambda: True + host.assigned_host_ports = _assigned_host_ports + host.build_success_payload = lambda x: {} + host.group_id = lambda: "85000000600A098000A4B9D1003637135D483DEB" + host.create_host() + + def test_remove_host_pass(self): + """Verify remove_host produces expected results.""" + with mock.patch(self.REQ_FUNC, return_value=(200, None)): + self._set_args({'state': 'absent', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False, 'group': 'test_group', + 'ports': [{'label': 'beegfs_metadata1_iscsi_0', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + host.host_obj = {"id": "84000000600A098000A4B9D10030370B5D430109"} + host.remove_host() + + def test_remove_host_fail(self): + """Verify remove_host produces expected exceptions.""" + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to remove host."): + with mock.patch(self.REQ_FUNC, return_value=Exception()): + self._set_args({'state': 'absent', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False, 'group': 'test_group', + 'ports': [{'label': 'beegfs_metadata1_iscsi_0', 'type': 'iscsi', + 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]}) + host = Host() + host.host_obj = {"id": "84000000600A098000A4B9D10030370B5D430109"} + host.remove_host() + + def test_build_success_payload(self): + """Validate success payload.""" + def _assigned_host_ports(apply_unassigning=False): + return None + + self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': True, 'group': 'test_group', + 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi', 'port': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818'}]}) host = Host() - host.host_obj = self.HOST - with mock.patch(self.REQ_FUNC, return_value=(200, [self.HOST])) as request: - needs_update = host.needs_update - self.assertTrue(needs_update, msg="An update to the host should be required!") - - def test_needs_update_cluster(self): - """Ensure a changed group_id triggers an update""" - self._set_args({ - 'state': 'present', - 'host_type': self.HOST['hostTypeIndex'], - 'group': '1', - }) - host = Host() - host.host_obj = self.HOST - with mock.patch(self.REQ_FUNC, return_value=(200, [self.HOST])) as request: - needs_update = host.needs_update - self.assertTrue(needs_update, msg="An update to the host should be required!") - - def test_needs_update_no_change(self): - """Ensure no changes do not trigger an update""" - self._set_args({ - 'state': 'present', - 'host_type': self.HOST['hostTypeIndex'], - }) - host = Host() - host.host_obj = self.HOST - with mock.patch(self.REQ_FUNC, return_value=(200, [self.HOST])) as request: - needs_update = host.needs_update - self.assertFalse(needs_update, msg="An update to the host should be required!") - - def test_needs_update_ports(self): - """Ensure added ports trigger an update""" - self._set_args({ - 'state': 'present', - 'host_type': self.HOST['hostTypeIndex'], - 'ports': [{'label': 'abc', 'type': 'iscsi', 'port': '0'}], - }) - host = Host() - host.host_obj = self.HOST - with mock.patch.object(host, 'all_hosts', [self.HOST]): - needs_update = host.needs_update - self.assertTrue(needs_update, msg="An update to the host should be required!") - - def test_needs_update_changed_ports(self): - """Ensure changed ports trigger an update""" - self._set_args({ - 'state': 'present', - 'host_type': self.HOST['hostTypeIndex'], - 'ports': [{'label': 'abc', 'type': 'iscsi', 'port': '0'}], - }) - host = Host() - host.host_obj = self.HOST.copy() - host.host_obj['hostSidePorts'] = [{'label': 'xyz', 'type': 'iscsi', 'port': '0', 'address': 'iqn:0'}] - - with mock.patch.object(host, 'all_hosts', [self.HOST]): - needs_update = host.needs_update - self.assertTrue(needs_update, msg="An update to the host should be required!") - - def test_needs_update_changed_negative(self): - """Ensure a ports update with no changes does not trigger an update""" - self._set_args({ - 'state': 'present', - 'host_type': self.HOST['hostTypeIndex'], - 'ports': [{'label': 'abc', 'type': 'iscsi', 'port': '0'}], - }) - host = Host() - host.host_obj = self.HOST.copy() - host.host_obj['hostSidePorts'] = [{'label': 'xyz', 'type': 'iscsi', 'port': '0', 'address': 'iqn:0'}] - - with mock.patch.object(host, 'all_hosts', [self.HOST]): - needs_update = host.needs_update - self.assertTrue(needs_update, msg="An update to the host should be required!") + self.assertEquals(host.build_success_payload(), {'api_url': 'http://localhost/', 'ssid': '1'})