#!/usr/bin/python # -*- coding: utf-8 -*- # # Dell EMC OpenManage Ansible Modules # Version 4.4.0 # Copyright (C) 2019-2021 Dell Inc. or its subsidiaries. All Rights Reserved. # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = r''' --- module: ome_template short_description: Create, modify, deploy, delete, export, import and clone a template on OpenManage Enterprise version_added: "2.0.0" description: "This module creates, modifies, deploys, deletes, exports, imports and clones a template on OpenManage Enterprise." extends_documentation_fragment: - dellemc.openmanage.ome_auth_options options: command: description: - C(create) creates a new template. - C(modify) modifies an existing template. - C(deploy) creates a template-deployment job. - C(delete) deletes an existing template. - C(export) exports an existing template. - C(import) creates a template from a specified configuration text in SCP XML format. - C(clone) creates a clone of a existing template. choices: [create, modify, deploy, delete, export, import, clone] default: create aliases: ['state'] type: str template_id: description: - ID of the existing template. - This option is applicable when I(command) is C(modify), C(deploy), C(delete) and C(export). - This option is mutually exclusive with I(template_name). type: int template_name: description: - Name of the existing template. - This option is applicable when I(command) is C(modify), C(deploy), C(delete) and C(export). - This option is mutually exclusive with I(template_id). type: str device_id: description: - >- Specify the list of targeted device ID(s) when I(command) is C(deploy). When I (command) is C(create), specify the ID of a single device. - Either I(device_id) or I(device_service_tag) is mandatory or both can be applicable. type: list elements: int default: [] device_service_tag: description: - >- Specify the list of targeted device service tags when I (command) is C(deploy). When I(command) is C(create), specify the service tag of a single device. - Either I(device_id) or I(device_service_tag) is mandatory or both can be applicable. type: list elements: str default: [] device_group_names: description: - Specify the list of groups when I (command) is C(deploy). - Provide at least one of the mandatory options I(device_id), I(device_service_tag), or I(device_group_names). type: list elements: str default: [] template_view_type: description: - Select the type of view of the OME template. - This is applicable when I(command) is C(create),C(clone) and C(import). choices: [Deployment, Compliance, Inventory, Sample, None] type: str default: Deployment attributes: type: dict description: - >- Payload data for the template operations. All the variables in this option are added as payload for C(create), C(modify), C(deploy), C(import), and C(clone) operations. It takes the following attributes. - >- Attributes: List of dictionaries of attributes (if any) to be modified in the deployment template. This is applicable when I(command) is C(deploy) and C(modify). - >- Name: Name of the template. This is mandatory when I(command) is C(create), C(import), C(clone), and optional when I(command) is C(modify). - >- Description: Description for the template. This is applicable when I(command) is C(create) or C(modify). - >- Fqdds: This allows to create a template using components from a specified reference server. One or more, of the following values must be specified in a comma-separated string: iDRAC, System, BIOS, NIC, LifeCycleController, RAID, and EventFilters. If none of the values are specified, the default value 'All' is selected. This is applicable when I (command) is C(create). - >- Options: Options to control device shutdown or end power state post template deployment. This is applicable for C(deploy) operation. - >- Schedule: Provides options to schedule the deployment task immediately, or at a specified time. This is applicable when I(command) is C(deploy). - >- NetworkBootIsoModel: Payload to specify the ISO deployment details. This is applicable when I(command) is C(deploy). - >- Content: The XML content of template. This is applicable when I(command) is C(import). - >- Type: Template type ID, indicating the type of device for which configuration is supported, such as chassis and servers. This is applicable when I(command) is C(import). - >- TypeId: Template type ID, indicating the type of device for which configuration is supported, such as chassis and servers. This is applicable when I(command) is C(create). - >- Refer OpenManage Enterprise API Reference Guide for more details. requirements: - "python >= 2.7.5" author: "Jagadeesh N V (@jagadeeshnv)" notes: - Run this module from a system that has direct access to DellEMC OpenManage Enterprise. - This module does not support C(check_mode). ''' EXAMPLES = r''' --- - name: Create a template from a reference device dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" device_id: 25123 attributes: Name: "New Template" Description: "New Template description" - name: Modify template name, description, and attribute value dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "modify" template_id: 12 attributes: Name: "New Custom Template" Description: "Custom Template Description" # Attributes to be modified in the template. # For information on any attribute id, use API /TemplateService/Templates(Id)/Views(Id)/AttributeViewDetails # This section is optional Attributes: - Id: 1234 Value: "Test Attribute" IsIgnored: false - name: Deploy template on multiple devices dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "deploy" template_id: 12 device_id: - 12765 - 10173 device_service_tag: - 'SVTG123' - 'SVTG456' - name: Deploy template on groups dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "deploy" template_id: 12 device_group_names: - server_group_1 - server_group_2 - name: Deploy template on multiple devices along with the attributes values to be modified on the target devices dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "deploy" template_id: 12 device_id: - 12765 - 10173 device_service_tag: - 'SVTG123' attributes: # Device specific attributes to be modified during deployment. # For information on any attribute id, use API /TemplateService/Templates(Id)/Views(Id)/AttributeViewDetails # This section is optional Attributes: # specific device where attribute to be modified at deployment run-time. # The DeviceId should be mentioned above in the 'device_id' section. # Service tags not allowed. - DeviceId: 12765 Attributes: - Id : 15645 Value : "0.0.0.0" IsIgnored : false - DeviceId: 10173 Attributes: - Id : 18968, Value : "hostname-1" IsIgnored : false - name: Deploy template and Operating System (OS) on multiple devices dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "deploy" template_id: 12 device_id: - 12765 device_service_tag: - 'SVTG123' attributes: # Include this to install OS on the devices. # This section is optional NetworkBootIsoModel: BootToNetwork: true ShareType: "NFS" IsoTimeout: 1 # allowable values(1,2,4,8,16) in hours IsoPath: "/home/iso_path/filename.iso" ShareDetail: IpAddress: "192.168.0.2" ShareName: "sharename" User: "share_user" Password: "share_password" Options: EndHostPowerState: 1 ShutdownType: 0 TimeToWaitBeforeShutdown: 300 Schedule: RunLater: true RunNow: false - name: "Deploy template on multiple devices and changes the device-level attributes. After the template is deployed, install OS using its image" dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "deploy" template_id: 12 device_id: - 12765 - 10173 device_service_tag: - 'SVTG123' - 'SVTG456' attributes: Attributes: - DeviceId: 12765 Attributes: - Id : 15645 Value : "0.0.0.0" IsIgnored : false - DeviceId: 10173 Attributes: - Id : 18968, Value : "hostname-1" IsIgnored : false NetworkBootIsoModel: BootToNetwork: true ShareType: "NFS" IsoTimeout: 1 # allowable values(1,2,4,8,16) in hours IsoPath: "/home/iso_path/filename.iso" ShareDetail: IpAddress: "192.168.0.2" ShareName: "sharename" User: "share_user" Password: "share_password" Options: EndHostPowerState: 1 ShutdownType: 0 TimeToWaitBeforeShutdown: 300 Schedule: RunLater: true RunNow: false - name: Delete template dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "delete" template_id: 12 - name: Export a template dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "export" template_id: 12 # Start of example to export template to a local xml file - name: Export template to a local xml file dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "export" template_name: "my_template" register: result - name: Save template into a file ansible.builtin.copy: content: "{{ result.Content}}" dest: "/path/to/exported_template.xml" # End of example to export template to a local xml file - name: Clone a template dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "clone" template_id: 12 attributes: Name: "New Cloned Template Name" - name: Import template from XML content dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "import" attributes: Name: "Imported Template Name" # Template Type from TemplateService/TemplateTypes Type: 2 # xml string content Content: "\n\nTrue\nClear\n \n\nReady \nNo\n\n \nReady\n No\n\n\n" - name: Import template from local XML file dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "import" attributes: Name: "Imported Template Name" Type: 2 Content: "{{ lookup('ansible.builtin.file.', '/path/to/xmlfile') }}" - name: "Deploy template and Operating System (OS) on multiple devices." dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "deploy" template_id: 12 device_id: - 12765 device_service_tag: - 'SVTG123' attributes: # Include this to install OS on the devices. # This section is optional NetworkBootIsoModel: BootToNetwork: true ShareType: "CIFS" IsoTimeout: 1 # allowable values(1,2,4,8,16) in hours IsoPath: "/home/iso_path/filename.iso" ShareDetail: IpAddress: "192.168.0.2" ShareName: "sharename" User: "share_user" Password: "share_password" Options: EndHostPowerState: 1 ShutdownType: 0 TimeToWaitBeforeShutdown: 300 Schedule: RunLater: true RunNow: false - name: Create a compliance template from reference device dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "create" device_service_tag: - "SVTG123" template_view_type: "Compliance" attributes: Name: "Configuration Compliance" Description: "Configuration Compliance Template" Fqdds: "BIOS" - name: Import a compliance template from XML file dellemc.openmanage.ome_template: hostname: "192.168.0.1" username: "username" password: "password" command: "import" template_view_type: "Compliance" attributes: Name: "Configuration Compliance" Content: "{{ lookup('ansible.builtin.file', './test.xml') }}" Type: 2 ''' RETURN = r''' --- msg: description: Overall status of the template operation. returned: always type: str sample: "Successfully created a template with ID 23" return_id: description: ID of the template for C(create), C(modify), C(import) and C(clone) or task created in case of C(deploy). returned: success, when I(command) is C(create), C(modify), C(import), C(clone) and C(deploy) type: int sample: 12 TemplateId: description: ID of the template for C(export). returned: success, when I(command) is C(export) type: int sample: 13 Content: description: XML content of the exported template. This content can be written to a xml file. returned: success, when I(command) is C(export) type: str sample: "\n\nTrue\nClear\n\n\n Ready\nNo\n \n\nReady \nNo\n\n\n" error_info: description: Details of the HTTP Error. returned: on HTTP error type: dict sample: { "error": { "code": "Base.1.0.GeneralError", "message": "A general error has occurred. See ExtendedInfo for more information.", "@Message.ExtendedInfo": [ { "MessageId": "GEN1234", "RelatedProperties": [], "Message": "Unable to process the request because an error occurred.", "MessageArgs": [], "Severity": "Critical", "Resolution": "Retry the operation. If the issue persists, contact your system administrator." } ] } } ''' import json from ssl import SSLError from ansible.module_utils.basic import AnsibleModule from ansible_collections.dellemc.openmanage.plugins.module_utils.ome import RestOME from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError from ansible.module_utils.urls import ConnectionError, SSLValidationError TEMPLATES_URI = "TemplateService/Templates" TEMPLATE_PATH = "TemplateService/Templates({template_id})" TEMPALTE_ACTION = "TemplateService/Actions/TemplateService.{op}" DEVICE_URI = "DeviceService/Devices" GROUP_URI = "GroupService/Groups" def get_group_devices_all(rest_obj, uri): total_items = [] next_link = uri while next_link: resp = rest_obj.invoke_request('GET', next_link) data = resp.json_data total_items.extend(data.get("value", [])) next_link_list = str(data.get('@odata.nextLink', '')).split('/api') next_link = next_link_list[-1] return total_items def get_group(rest_obj, module, group_name): query_param = {"$filter": "Name eq '{0}'".format(group_name)} group_req = rest_obj.invoke_request("GET", GROUP_URI, query_param=query_param) for grp in group_req.json_data.get('value'): if grp['Name'] == group_name: return grp module.fail_json(msg="Group name '{0}' is invalid. Please provide a valid group name.".format(group_name)) def get_group_details(rest_obj, module): group_name_list = module.params.get('device_group_names') device_ids = [] for group_name in group_name_list: group = get_group(rest_obj, module, group_name) group_uri = GROUP_URI + "({0})/Devices".format(group['Id']) group_device_list = get_group_devices_all(rest_obj, group_uri) device_ids.extend([dev['Id'] for dev in group_device_list]) return device_ids def get_device_ids(module, rest_obj): """Getting the list of device ids filtered from the device inventory.""" target_ids = [] if module.params.get('device_service_tag') or module.params.get('device_id'): # device_list = get_group_devices_all(rest_obj, DEVICE_URI) device_list = rest_obj.get_all_report_details(DEVICE_URI)['report_list'] device_tag_id_map = dict([(device.get('DeviceServiceTag'), device.get('Id')) for device in device_list]) device_id = module.params.get('device_id') invalid_ids = set(device_id) - set(device_tag_id_map.values()) if invalid_ids: fail_module(module, msg="Unable to complete the operation because the entered target device" " id(s) '{0}' are invalid.".format(",".join(list(map(str, set(invalid_ids)))))) target_ids.extend(device_id) service_tags = module.params.get('device_service_tag') invalid_tags = set(service_tags) - set(device_tag_id_map.keys()) if invalid_tags: fail_module(module, msg="Unable to complete the operation because the entered target service" " tag(s) '{0}' are invalid.".format(",".join(set(invalid_tags)))) for tag in service_tags: # append ids for service tags target_ids.append(device_tag_id_map.get(tag)) if module.params.get('device_group_names'): target_ids.extend(get_group_details(rest_obj, module)) return list(set(target_ids)) # set to eliminate duplicates def get_view_id(rest_obj, viewstr): resp = rest_obj.invoke_request('GET', "TemplateService/TemplateViewTypes") if resp.success and resp.json_data.get('value'): tlist = resp.json_data.get('value', []) for xtype in tlist: if xtype.get('Description', "") == viewstr: return xtype.get('Id') viewmap = {"Deployment": 2, "Compliance": 1, "Inventory": 3, "Sample": 4, "None": 0} return viewmap.get(viewstr) def get_type_id_valid(rest_obj, typeid): resp = rest_obj.invoke_request('GET', "TemplateService/TemplateTypes") if resp.success and resp.json_data.get('value'): tlist = resp.json_data.get('value', []) for xtype in tlist: if xtype.get('Id') == typeid: # use Name if str is passed return True return False def get_create_payload(module_params, deviceid, view_id): create_payload = {"Fqdds": "All", "ViewTypeId": view_id} if isinstance(module_params.get("attributes"), dict): attrib_dict = module_params.get("attributes").copy() typeid = attrib_dict.get("Type") and attrib_dict.get("Type") or attrib_dict.get("TypeId") if typeid: create_payload["TypeId"] = typeid attrib_dict.pop("Type", None) # remove if exists as it is not required for create payload create_payload.update(attrib_dict) create_payload["SourceDeviceId"] = int(deviceid) return create_payload def get_modify_payload(module_params, template_id, template_dict): modify_payload = {} if isinstance(module_params.get("attributes"), dict): modify_payload.update(module_params.get("attributes")) modify_payload['Id'] = template_id # Update with old template values if not modify_payload.get("Name"): modify_payload["Name"] = template_dict["Name"] if not modify_payload.get("Description"): modify_payload["Description"] = template_dict["Description"] return modify_payload def get_deploy_payload(module_params, deviceidlist, template_id): deploy_payload = {} if isinstance(module_params.get("attributes"), dict): deploy_payload.update(module_params.get("attributes")) deploy_payload["Id"] = template_id deploy_payload["TargetIds"] = deviceidlist return deploy_payload def get_import_payload(module, rest_obj, view_id): attrib_dict = module.params.get("attributes").copy() import_payload = {} import_payload["Name"] = attrib_dict.pop("Name") import_payload["ViewTypeId"] = view_id import_payload["Type"] = 2 typeid = attrib_dict.get("Type") and attrib_dict.get("Type") or attrib_dict.get("TypeId") if typeid: if get_type_id_valid(rest_obj, typeid): import_payload["Type"] = typeid # Type is mandatory for import else: fail_module(module, msg="Type provided for 'import' operation is invalid") import_payload["Content"] = attrib_dict.pop("Content") if isinstance(attrib_dict, dict): attrib_dict.pop("TypeId", None) # remove if exists as it is not required for import payload import_payload.update(attrib_dict) return import_payload def get_clone_payload(module_params, template_id, view_id): attrib_dict = module_params.get("attributes").copy() clone_payload = {} clone_payload["SourceTemplateId"] = template_id clone_payload["NewTemplateName"] = attrib_dict.pop("Name") clone_payload["ViewTypeId"] = view_id if isinstance(attrib_dict, dict): clone_payload.update(attrib_dict) return clone_payload def get_template_by_id(module, rest_obj, template_id): path = TEMPLATE_PATH.format(template_id=template_id) template_req = rest_obj.invoke_request("GET", path) if template_req.success: return template_req.json_data else: fail_module(module, msg="Unable to complete the operation because the" " requested template is not present.") def get_template_by_name(template_name, module, rest_obj): """Filter out specific template based on name, and it returns template_id. :param template_name: string :param module: dictionary :param rest_obj: object :return: template_id: integer """ template_id = None template = None template_path = TEMPLATES_URI query_param = {"$filter": "Name eq '{0}'".format(template_name)} template_req = rest_obj.invoke_request("GET", template_path, query_param=query_param) for each in template_req.json_data.get('value'): if each['Name'] == template_name: template_id = each['Id'] template = each break else: fail_module(module, msg="Unable to complete the operation because the" " requested template with name {0} is not present.".format(template_name)) return template, template_id def _get_resource_parameters(module, rest_obj): command = module.params.get("command") rest_method = 'POST' payload = {} template_id = module.params.get("template_id") template_name = module.params.get("template_name") if template_name: template, template_id = get_template_by_name(template_name, module, rest_obj) if command not in ["import", "create"] and template_id is None: fail_module(module, msg="Enter a valid template_name or template_id") if command == "create": devid_list = get_device_ids(module, rest_obj) if len(devid_list) != 1: fail_module(module, msg="Create template requires only one reference device") view_id = get_view_id(rest_obj, module.params['template_view_type']) payload = get_create_payload(module.params, devid_list[0], view_id) path = TEMPLATES_URI elif command == "modify": path = TEMPLATE_PATH.format(template_id=template_id) template_dict = get_template_by_id(module, rest_obj, template_id) payload = get_modify_payload(module.params, template_id, template_dict) rest_method = 'PUT' elif command == "delete": path = TEMPLATE_PATH.format(template_id=template_id) rest_method = 'DELETE' elif command == "export": path = TEMPALTE_ACTION.format(op="Export") payload = {'TemplateId': template_id} elif command == "deploy": devid_list = get_device_ids(module, rest_obj) if not devid_list: fail_module(module, msg="There are no devices provided for deploy operation") path = TEMPALTE_ACTION.format(op="Deploy") payload = get_deploy_payload(module.params, devid_list, template_id) elif command == "clone": view_id = get_view_id(rest_obj, module.params['template_view_type']) path = TEMPALTE_ACTION.format(op="Clone") payload = get_clone_payload(module.params, template_id, view_id) else: view_id = get_view_id(rest_obj, module.params['template_view_type']) path = TEMPALTE_ACTION.format(op="Import") payload = get_import_payload(module, rest_obj, view_id) return path, payload, rest_method def _validate_inputs(module): """validates input parameters""" command = module.params.get("command") if command in ["create", "deploy"]: dev_id = module.params["device_id"] dev_st = module.params["device_service_tag"] if None in dev_id or None in dev_st: fail_module(module, msg="Argument device_id or device_service_tag has null values") attrib_dict = {} if module.params.get("attributes"): attrib_dict = module.params.get("attributes") if command in ["import", "clone", "create"]: if not attrib_dict.get("Name"): fail_module(module, msg="Argument 'Name' required in attributes for {0} operation".format(command)) if command == "import": if not attrib_dict.get("Content"): fail_module(module, msg="Argument 'Content' required in attributes for {0} operation".format(command)) def password_no_log(attributes): if isinstance(attributes, dict): netdict = attributes.get("NetworkBootIsoModel") if isinstance(netdict, dict): sharedet = netdict.get("ShareDetail") if isinstance(sharedet, dict) and 'Password' in sharedet: sharedet['Password'] = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" def fail_module(module, **failmsg): password_no_log(module.params.get("attributes")) module.fail_json(**failmsg) def exit_module(module, response): password_no_log(module.params.get("attributes")) resp = None my_change = True command = module.params.get('command') result = {} if command in ["create", "modify", "deploy", "import", "clone"]: result["return_id"] = response.json_data resp = result["return_id"] if command == 'deploy' and result["return_id"] == 0: result["failed"] = True command = 'deploy_fail' my_change = False if command == 'export': my_change = False result = response.json_data msg_dict = {'create': "Successfully created a template with ID {0}".format(resp), 'modify': "Successfully modified the template with ID {0}".format(resp), 'deploy': "Successfully created the template-deployment job with ID {0}".format(resp), 'deploy_fail': 'Failed to deploy template.', 'delete': "Deleted successfully", 'export': "Exported successfully", 'import': "Imported successfully", 'clone': "Cloned successfully"} module.exit_json(msg=msg_dict.get(command), changed=my_change, **result) def main(): module = AnsibleModule( argument_spec={ "hostname": {"required": True, "type": 'str'}, "username": {"required": True, "type": 'str'}, "password": {"required": True, "type": 'str', "no_log": True}, "port": {"required": False, "default": 443, "type": 'int'}, "command": {"required": False, "default": "create", "aliases": ['state'], "choices": ['create', 'modify', 'deploy', 'delete', 'export', 'import', 'clone']}, "template_id": {"required": False, "type": 'int'}, "template_name": {"required": False, "type": 'str'}, "template_view_type": {"required": False, "default": 'Deployment', "choices": ['Deployment', 'Compliance', 'Inventory', 'Sample', 'None']}, "device_id": {"required": False, "type": 'list', "default": [], "elements": 'int'}, "device_service_tag": {"required": False, "type": 'list', "default": [], "elements": 'str'}, "device_group_names": {"required": False, "type": 'list', "default": [], "elements": 'str'}, "attributes": {"required": False, "type": 'dict'}, }, required_if=[ ['command', 'create', ['attributes']], ['command', 'modify', ['attributes']], ['command', 'import', ['attributes']], ['command', 'modify', ['template_id', 'template_name'], True], ['command', 'delete', ['template_id', 'template_name'], True], ['command', 'export', ['template_id', 'template_name'], True], ['command', 'clone', ['template_id', 'template_name'], True], ['command', 'deploy', ['template_id', 'template_name'], True], ['command', 'deploy', ['device_id', 'device_service_tag', 'device_group_names'], True], ], mutually_exclusive=[["template_id", "template_name"]], supports_check_mode=False) try: _validate_inputs(module) with RestOME(module.params, req_session=True) as rest_obj: path, payload, rest_method = _get_resource_parameters(module, rest_obj) resp = rest_obj.invoke_request(rest_method, path, data=payload) if resp.success: exit_module(module, resp) except HTTPError as err: fail_module(module, msg=str(err), error_info=json.load(err)) except URLError as err: password_no_log(module.params.get("attributes")) module.exit_json(msg=str(err), unreachable=True) except (IOError, SSLError, SSLValidationError, ConnectionError, TypeError, ValueError, KeyError) as err: fail_module(module, msg=str(err)) if __name__ == '__main__': main()