#!/usr/bin/python # Copyright: (c) 2022, Dell Technologies # Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt) """Ansible module for managing CIFS server on Unity""" from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = r''' module: cifsserver version_added: '1.4.0' short_description: Manage CIFS server on Unity storage system description: - Managing the CIFS server on the Unity storage system includes creating CIFS server, getting CIFS server details and deleting CIFS server. extends_documentation_fragment: - dellemc.unity.unity author: - Akash Shendge (@shenda1) options: nas_server_name: description: - Name of the NAS server on which CIFS server will be hosted. type: str nas_server_id: description: - ID of the NAS server on which CIFS server will be hosted. type: str netbios_name: description: - The computer name of the SMB server in Windows network. type: str workgroup: description: - Standalone SMB server workgroup. type: str local_password: description: - Standalone SMB server administrator password. type: str domain: description: - The domain name where the SMB server is registered in Active Directory. type: str domain_username: description: - Active Directory domain user name. type: str domain_password: description: - Active Directory domain password. type: str cifs_server_name: description: - The name of the CIFS server. type: str cifs_server_id: description: - The ID of the CIFS server. type: str interfaces: description: - List of file IP interfaces that service CIFS protocol of SMB server. type: list elements: str unjoin_cifs_server_account: description: - Keep SMB server account unjoined in Active Directory after deletion. - C(false) specifies keep SMB server account joined after deletion. - C(true) specifies unjoin SMB server account from Active Directory before deletion. type: bool state: description: - Define whether the CIFS server should exist or not. choices: [absent, present] required: true type: str notes: - The I(check_mode) is supported. ''' EXAMPLES = r''' - name: Create CIFS server belonging to Active Directory dellemc.unity.cifsserver: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" nas_server_name: "test_nas1" cifs_server_name: "test_cifs" domain: "ad_domain" domain_username: "domain_username" domain_password: "domain_password" state: "present" - name: Get CIFS server details using CIFS server ID dellemc.unity.cifsserver: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" cifs_server_id: "cifs_37" state: "present" - name: Get CIFS server details using NAS server name dellemc.unity.cifsserver: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" nas_server_name: "test_nas1" state: "present" - name: Delete CIFS server dellemc.unity.cifsserver: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" cifs_server_id: "cifs_37" unjoin_cifs_server_account: true domain_username: "domain_username" domain_password: "domain_password" state: "absent" - name: Create standalone CIFS server dellemc.unity.cifsserver: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" netbios_name: "ANSIBLE_CIFS" workgroup: "ansible" local_password: "Password123!" nas_server_name: "test_nas1" state: "present" - name: Get CIFS server details using netbios name dellemc.unity.cifsserver: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" netbios_name: "ANSIBLE_CIFS" state: "present" - name: Delete standalone CIFS server dellemc.unity.cifsserver: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" cifs_server_id: "cifs_40" state: "absent" ''' RETURN = r''' changed: description: Whether or not the resource has changed. returned: always type: bool sample: true cifs_server_details: description: Details of the CIFS server. returned: When CIFS server exists type: dict contains: id: description: Unique identifier of the CIFS server instance. type: str name: description: User-specified name for the SMB server. type: str netbios_name: description: Computer Name of the SMB server in windows network. type: str description: description: Description of the SMB server. type: str domain: description: Domain name where SMB server is registered in Active Directory. type: str workgroup: description: Windows network workgroup for the SMB server. type: str is_standalone: description: Indicates whether the SMB server is standalone. type: bool nasServer: description: Information about the NAS server in the storage system. type: dict contains: UnityNasServer: description: Information about the NAS server in the storage system. type: dict contains: id: description: Unique identifier of the NAS server instance. type: str file_interfaces: description: The file interfaces associated with the NAS server. type: dict contains: UnityFileInterfaceList: description: List of file interfaces associated with the NAS server. type: list contains: UnityFileInterface: description: Details of file interface associated with the NAS server. type: dict contains: id: description: Unique identifier of the file interface. type: str smb_multi_channel_supported: description: Indicates whether the SMB 3.0+ multichannel feature is supported. type: bool smb_protocol_versions: description: Supported SMB protocols, such as 1.0, 2.0, 2.1, 3.0, and so on. type: list smbca_supported: description: Indicates whether the SMB server supports continuous availability. type: bool sample: { "description": null, "domain": "xxx.xxx.xxx.com", "existed": true, "file_interfaces": { "UnityFileInterfaceList": [ { "UnityFileInterface": { "hash": -9223363258905013637, "id": "if_43" } } ] }, "hash": -9223363258905010379, "health": { "UnityHealth": { "hash": 8777949765559 } }, "id": "cifs_40", "is_standalone": false, "last_used_organizational_unit": "ou=Computers,ou=Dell NAS servers", "name": "ansible_cifs", "nas_server": { "UnityNasServer": { "hash": 8777949765531, "id": "nas_18" } }, "netbios_name": "ANSIBLE_CIFS", "smb_multi_channel_supported": true, "smb_protocol_versions": [ "1.0", "2.0", "2.1", "3.0" ], "smbca_supported": true, "workgroup": null } ''' from ansible.module_utils.basic import AnsibleModule from ansible_collections.dellemc.unity.plugins.module_utils.storage.dell import utils LOG = utils.get_logger('cifsserver') application_type = "Ansible/1.7.1" class CIFSServer(object): """Class with CIFS server operations""" def __init__(self): """Define all parameters required by this module""" self.module_params = utils.get_unity_management_host_parameters() self.module_params.update(get_cifs_server_parameters()) mutually_exclusive = [['nas_server_name', 'nas_server_id'], ['cifs_server_id', 'cifs_server_name'], ['cifs_server_id', 'netbios_name']] required_one_of = [['cifs_server_id', 'cifs_server_name', 'netbios_name', 'nas_server_name', 'nas_server_id']] # initialize the Ansible module self.module = AnsibleModule( argument_spec=self.module_params, supports_check_mode=True, mutually_exclusive=mutually_exclusive, required_one_of=required_one_of ) utils.ensure_required_libs(self.module) self.unity_conn = utils.get_unity_unisphere_connection( self.module.params, application_type) LOG.info('Check Mode Flag %s', self.module.check_mode) def get_details(self, cifs_server_id=None, cifs_server_name=None, netbios_name=None, nas_server_id=None): """Get CIFS server details. :param: cifs_server_id: The ID of the CIFS server :param: cifs_server_name: The name of the CIFS server :param: netbios_name: Name of the SMB server in windows network :param: nas_server_id: The ID of the NAS server :return: Dict containing CIFS server details if exists """ LOG.info("Getting CIFS server details") id_or_name = get_id_name(cifs_server_id, cifs_server_name, netbios_name, nas_server_id) try: if cifs_server_id: cifs_server_details = self.unity_conn.get_cifs_server(_id=cifs_server_id) return process_response(cifs_server_details) if cifs_server_name: cifs_server_details = self.unity_conn.get_cifs_server(name=cifs_server_name) return process_response(cifs_server_details) if netbios_name: cifs_server_details = self.unity_conn.get_cifs_server(netbios_name=netbios_name) if len(cifs_server_details) > 0: return process_dict(cifs_server_details._get_properties()) if nas_server_id: cifs_server_details = self.unity_conn.get_cifs_server(nas_server=nas_server_id) if len(cifs_server_details) > 0: return process_dict(cifs_server_details._get_properties()) return None except utils.HttpError as e: if e.http_status == 401: msg = "Failed to get CIFS server: %s due to incorrect " \ "username/password error: %s" % (id_or_name, str(e)) else: msg = "Failed to get CIFS server: %s with error: %s" % (id_or_name, str(e)) except utils.UnityResourceNotFoundError: msg = "CIFS server with ID %s not found" % cifs_server_id LOG.info(msg) return None except utils.StoropsConnectTimeoutError as e: msg = "Failed to get CIFS server: %s with error: %s. Please check unispherehost IP: %s" % ( id_or_name, str(e), self.module.params['unispherehost']) except Exception as e: msg = "Failed to get details of CIFS server: %s with error: %s" % (id_or_name, str(e)) LOG.error(msg) self.module.fail_json(msg=msg) def get_cifs_server_instance(self, cifs_server_id): """Get CIFS server instance. :param: cifs_server_id: The ID of the CIFS server :return: Return CIFS server instance if exists """ try: cifs_server_obj = utils.UnityCifsServer.get(cli=self.unity_conn._cli, _id=cifs_server_id) return cifs_server_obj except Exception as e: error_msg = "Failed to get the CIFS server %s instance" \ " with error %s" % (cifs_server_id, str(e)) LOG.error(error_msg) self.module.fail_json(msg=error_msg) def delete_cifs_server(self, cifs_server_id, skip_unjoin=None, domain_username=None, domain_password=None): """Delete CIFS server. :param: cifs_server_id: The ID of the CIFS server :param: skip_unjoin: Flag indicating whether to unjoin SMB server account from AD before deletion :param: domain_username: The domain username :param: domain_password: The domain password :return: Return True if CIFS server is deleted """ LOG.info("Deleting CIFS server") try: if not self.module.check_mode: cifs_obj = self.get_cifs_server_instance(cifs_server_id=cifs_server_id) cifs_obj.delete(skip_domain_unjoin=skip_unjoin, username=domain_username, password=domain_password) return True except Exception as e: msg = "Failed to delete CIFS server: %s with error: %s" % (cifs_server_id, str(e)) LOG.error(msg) self.module.fail_json(msg=msg) def get_nas_server_id(self, nas_server_name): """Get NAS server ID. :param: nas_server_name: The name of NAS server :return: Return NAS server ID if exists """ LOG.info("Getting NAS server ID") try: obj_nas = self.unity_conn.get_nas_server(name=nas_server_name) return obj_nas.get_id() except Exception as e: msg = "Failed to get details of NAS server: %s with error: %s" % (nas_server_name, str(e)) LOG.error(msg) self.module.fail_json(msg=msg) def is_modify_interfaces(self, cifs_server_details): """Check if modification is required in existing interfaces :param: cifs_server_details: CIFS server details :return: Flag indicating if modification is required """ existing_interfaces = [] if cifs_server_details['file_interfaces']['UnityFileInterfaceList']: for interface in cifs_server_details['file_interfaces']['UnityFileInterfaceList']: existing_interfaces.append(interface['UnityFileInterface']['id']) for interface in self.module.params['interfaces']: if interface not in existing_interfaces: return True return False def is_modification_required(self, cifs_server_details): """Check if modification is required in existing CIFS server :param: cifs_server_details: CIFS server details :return: Flag indicating if modification is required """ LOG.info("Checking if any modification is required") param_list = ['netbios_name', 'workgroup'] for param in param_list: if self.module.params[param] is not None and cifs_server_details[param] is not None and \ self.module.params[param].upper() != cifs_server_details[param]: return True # Check for domain if self.module.params['domain'] is not None and cifs_server_details['domain'] is not None and \ self.module.params['domain'] != cifs_server_details['domain']: return True # Check file interfaces if self.module.params['interfaces'] is not None: return self.is_modify_interfaces(cifs_server_details) return False def create_cifs_server(self, nas_server_id, interfaces=None, netbios_name=None, cifs_server_name=None, domain=None, domain_username=None, domain_password=None, workgroup=None, local_password=None): """Create CIFS server. :param: nas_server_id: The ID of NAS server :param: interfaces: List of file interfaces :param: netbios_name: Name of the SMB server in windows network :param: cifs_server_name: Name of the CIFS server :param: domain: The domain name where the SMB server is registered in Active Directory :param: domain_username: The domain username :param: domain_password: The domain password :param: workgroup: Standalone SMB server workgroup :param: local_password: Standalone SMB server admin password :return: Return True if CIFS server is created """ LOG.info("Creating CIFS server") try: if not self.module.check_mode: utils.UnityCifsServer.create(cli=self.unity_conn._cli, nas_server=nas_server_id, interfaces=interfaces, netbios_name=netbios_name, name=cifs_server_name, domain=domain, domain_username=domain_username, domain_password=domain_password, workgroup=workgroup, local_password=local_password) return True except Exception as e: msg = "Failed to create CIFS server with error: %s" % (str(e)) LOG.error(msg) self.module.fail_json(msg=msg) def validate_params(self): """Validate the parameters """ param_list = ['nas_server_id', 'nas_server_name', 'domain', 'cifs_server_id', 'cifs_server_name', 'local_password', 'netbios_name', 'workgroup', 'domain_username', 'domain_password'] msg = "Please provide valid {0}" for param in param_list: if self.module.params[param] is not None and len(self.module.params[param].strip()) == 0: errmsg = msg.format(param) self.module.fail_json(msg=errmsg) def perform_module_operation(self): """ Perform different actions on CIFS server module based on parameters passed in the playbook """ cifs_server_id = self.module.params['cifs_server_id'] cifs_server_name = self.module.params['cifs_server_name'] nas_server_id = self.module.params['nas_server_id'] nas_server_name = self.module.params['nas_server_name'] netbios_name = self.module.params['netbios_name'] workgroup = self.module.params['workgroup'] local_password = self.module.params['local_password'] domain = self.module.params['domain'] domain_username = self.module.params['domain_username'] domain_password = self.module.params['domain_password'] interfaces = self.module.params['interfaces'] unjoin_cifs_server_account = self.module.params['unjoin_cifs_server_account'] state = self.module.params['state'] # result is a dictionary that contains changed status and CIFS server details result = dict( changed=False, cifs_server_details={} ) # Validate the parameters self.validate_params() if nas_server_name is not None: nas_server_id = self.get_nas_server_id(nas_server_name) cifs_server_details = self.get_details(cifs_server_id=cifs_server_id, cifs_server_name=cifs_server_name, netbios_name=netbios_name, nas_server_id=nas_server_id) # Check if modification is required if cifs_server_details: if cifs_server_id is None: cifs_server_id = cifs_server_details['id'] modify_flag = self.is_modification_required(cifs_server_details) if modify_flag: self.module.fail_json(msg="Modification is not supported through Ansible module") if not cifs_server_details and state == 'present': if not nas_server_id: self.module.fail_json(msg="Please provide nas server id/name to create CIFS server.") if any([netbios_name, workgroup, local_password]) and not all([netbios_name, workgroup, local_password]): msg = "netbios_name, workgroup and local_password " \ "are required to create standalone CIFS server." LOG.error(msg) self.module.fail_json(msg=msg) result['changed'] = self.create_cifs_server(nas_server_id, interfaces, netbios_name, cifs_server_name, domain, domain_username, domain_password, workgroup, local_password) if state == 'absent' and cifs_server_details: skip_unjoin = None if unjoin_cifs_server_account is not None: skip_unjoin = not unjoin_cifs_server_account result['changed'] = self.delete_cifs_server(cifs_server_id, skip_unjoin, domain_username, domain_password) if state == 'present': result['cifs_server_details'] = self.get_details(cifs_server_id=cifs_server_id, cifs_server_name=cifs_server_name, netbios_name=netbios_name, nas_server_id=nas_server_id) LOG.info("Process Dict: %s", result['cifs_server_details']) self.module.exit_json(**result) def get_id_name(cifs_server_id=None, cifs_server_name=None, netbios_name=None, nas_server_id=None): """Get the id_or_name. :param: cifs_server_id: The ID of CIFS server :param: cifs_server_name: The name of CIFS server :param: netbios_name: Name of the SMB server in windows network :param: nas_server_id: The ID of NAS server :return: Return id_or_name """ if cifs_server_id: id_or_name = cifs_server_id elif cifs_server_name: id_or_name = cifs_server_name elif netbios_name: id_or_name = netbios_name else: id_or_name = nas_server_id return id_or_name def process_response(cifs_server_details): """Process CIFS server details. :param: cifs_server_details: Dict containing CIFS server details :return: Processed dict containing CIFS server details """ if cifs_server_details.existed: return cifs_server_details._get_properties() def process_dict(cifs_server_details): """Process CIFS server details. :param: cifs_server_details: Dict containing CIFS server details :return: Processed dict containing CIFS server details """ param_list = ['description', 'domain', 'file_interfaces', 'health', 'id', 'is_standalone', 'name', 'nas_server' 'netbios_name', 'smb_multi_channel_supported', 'smb_protocol_versions', 'smbca_supported', 'workgroup', 'netbios_name'] for param in param_list: if param in cifs_server_details: cifs_server_details[param] = cifs_server_details[param][0] return cifs_server_details def get_cifs_server_parameters(): """This method provide parameters required for the ansible CIFS server module on Unity""" return dict( cifs_server_id=dict(), cifs_server_name=dict(), netbios_name=dict(), workgroup=dict(), local_password=dict(no_log=True), domain=dict(), domain_username=dict(), domain_password=dict(no_log=True), nas_server_name=dict(), nas_server_id=dict(), interfaces=dict(type='list', elements='str'), unjoin_cifs_server_account=dict(type='bool'), state=dict(required=True, type='str', choices=['present', 'absent']), ) def main(): """Create Unity CIFS server object and perform action on it based on user input from playbook""" obj = CIFSServer() obj.perform_module_operation() if __name__ == '__main__': main()