diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:03:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:03:42 +0000 |
commit | 66cec45960ce1d9c794e9399de15c138acb18aed (patch) | |
tree | 59cd19d69e9d56b7989b080da7c20ef1a3fe2a5a /ansible_collections/cisco/intersight/plugins | |
parent | Initial commit. (diff) | |
download | ansible-66cec45960ce1d9c794e9399de15c138acb18aed.tar.xz ansible-66cec45960ce1d9c794e9399de15c138acb18aed.zip |
Adding upstream version 7.3.0+dfsg.upstream/7.3.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/cisco/intersight/plugins')
12 files changed, 2926 insertions, 0 deletions
diff --git a/ansible_collections/cisco/intersight/plugins/README.md b/ansible_collections/cisco/intersight/plugins/README.md new file mode 100644 index 00000000..6541cf7c --- /dev/null +++ b/ansible_collections/cisco/intersight/plugins/README.md @@ -0,0 +1,31 @@ +# Collections Plugins Directory + +This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that +is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that +would contain module utils and modules respectively. + +Here is an example directory of the majority of plugins currently supported by Ansible: + +``` +└── plugins + ├── action + ├── become + ├── cache + ├── callback + ├── cliconf + ├── connection + ├── filter + ├── httpapi + ├── inventory + ├── lookup + ├── module_utils + ├── modules + ├── netconf + ├── shell + ├── strategy + ├── terminal + ├── test + └── vars +``` + +A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible/2.9/plugins/plugins.html).
\ No newline at end of file diff --git a/ansible_collections/cisco/intersight/plugins/doc_fragments/intersight.py b/ansible_collections/cisco/intersight/plugins/doc_fragments/intersight.py new file mode 100644 index 00000000..a9843d46 --- /dev/null +++ b/ansible_collections/cisco/intersight/plugins/doc_fragments/intersight.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2016 Red Hat Inc. +# (c) 2017 Cisco Systems Inc. +# +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) +# + + +class ModuleDocFragment(object): + # Cisco Intersight doc fragment + DOCUMENTATION = ''' +options: + api_private_key: + description: + - 'Filename (absolute path) or string of PEM formatted private key data to be used for Intersight API authentication.' + - If a string is used, Ansible vault should be used to encrypt string data. + - "Ex. ansible-vault encrypt_string --vault-id tme@/Users/dsoper/Documents/vault_password_file '-----BEGIN EC PRIVATE KEY-----" + - " <your private key data>" + - " -----END EC PRIVATE KEY-----'" + - If not set, the value of the INTERSIGHT_API_PRIVATE_KEY environment variable is used. + type: str + required: yes + api_uri: + description: + - URI used to access the Intersight API. + - If not set, the value of the INTERSIGHT_API_URI environment variable is used. + type: str + default: https://intersight.com/api/v1 + api_key_id: + description: + - Public API Key ID associated with the private key. + - If not set, the value of the INTERSIGHT_API_KEY_ID environment variable is used. + type: str + required: yes + validate_certs: + description: + - Boolean control for verifying the api_uri TLS certificate + type: bool + default: yes + use_proxy: + description: + - If C(no), it will not use a proxy, even if one is defined in an environment variable on the target hosts. + type: bool + default: yes +''' diff --git a/ansible_collections/cisco/intersight/plugins/module_utils/intersight.py b/ansible_collections/cisco/intersight/plugins/module_utils/intersight.py new file mode 100644 index 00000000..4bfd0e93 --- /dev/null +++ b/ansible_collections/cisco/intersight/plugins/module_utils/intersight.py @@ -0,0 +1,471 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2016 Red Hat Inc. +# (c) 2020 Cisco Systems Inc. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# Intersight REST API Module +# Author: Matthew Garrett +# Contributors: David Soper, Chris Gascoigne, John McDonough + +from base64 import b64encode +from email.utils import formatdate +import re +import json +import hashlib +from ansible.module_utils.six import iteritems +from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode +from ansible.module_utils.urls import fetch_url +from ansible.module_utils.basic import env_fallback + +try: + from cryptography.hazmat.primitives import serialization, hashes + from cryptography.hazmat.primitives.asymmetric import padding, ec + from cryptography.hazmat.backends import default_backend + HAS_CRYPTOGRAPHY = True +except ImportError: + HAS_CRYPTOGRAPHY = False + +intersight_argument_spec = dict( + api_private_key=dict(fallback=(env_fallback, ['INTERSIGHT_API_PRIVATE_KEY']), type='path', required=True, no_log=True), + api_uri=dict(fallback=(env_fallback, ['INTERSIGHT_API_URI']), type='str', default='https://intersight.com/api/v1'), + api_key_id=dict(fallback=(env_fallback, ['INTERSIGHT_API_KEY_ID']), type='str', required=True), + validate_certs=dict(type='bool', default=True), + use_proxy=dict(type='bool', default=True), +) + + +def get_sha256_digest(data): + """ + Generates a SHA256 digest from a String. + + :param data: data string set by user + :return: instance of digest object + """ + + digest = hashlib.sha256() + digest.update(data.encode()) + + return digest + + +def prepare_str_to_sign(req_tgt, hdrs): + """ + Concatenates Intersight headers in preparation to be signed + + :param req_tgt : http method plus endpoint + :param hdrs: dict with header keys + :return: concatenated header authorization string + """ + ss = "" + ss = ss + "(request-target): " + req_tgt + "\n" + + length = len(hdrs.items()) + + i = 0 + for key, value in hdrs.items(): + ss = ss + key.lower() + ": " + value + if i < length - 1: + ss = ss + "\n" + i += 1 + + return ss + + +def get_gmt_date(): + """ + Generated a GMT formatted Date + + :return: current date + """ + + return formatdate(timeval=None, localtime=False, usegmt=True) + + +def compare_lists(expected_list, actual_list): + if len(expected_list) != len(actual_list): + # mismatch if list lengths aren't equal + return False + for expected, actual in zip(expected_list, actual_list): + # if compare_values returns False, stop the loop and return + if not compare_values(expected, actual): + return False + # loop complete with all items matching + return True + + +def compare_values(expected, actual): + try: + if isinstance(expected, list) and isinstance(actual, list): + return compare_lists(expected, actual) + for (key, value) in iteritems(expected): + if re.search(r'P(ass)?w(or)?d', key) or key not in actual: + # do not compare any password related attributes or attributes that are not in the actual resource + continue + if not compare_values(value, actual[key]): + return False + # loop complete with all items matching + return True + except (AttributeError, TypeError): + # if expected and actual != expected: + if actual != expected: + return False + return True + + +class IntersightModule(): + + def __init__(self, module): + self.module = module + self.result = dict(changed=False) + if not HAS_CRYPTOGRAPHY: + self.module.fail_json(msg='cryptography is required for this module') + self.host = self.module.params['api_uri'] + self.public_key = self.module.params['api_key_id'] + try: + with open(self.module.params['api_private_key'], 'r') as f: + self.private_key = f.read() + except (FileNotFoundError, OSError): + self.private_key = self.module.params['api_private_key'] + self.digest_algorithm = '' + self.response_list = [] + + def get_sig_b64encode(self, data): + """ + Generates a signed digest from a String + + :param digest: string to be signed & hashed + :return: instance of digest object + """ + # Python SDK code: Verify PEM Pre-Encapsulation Boundary + r = re.compile(r"\s*-----BEGIN (.*)-----\s+") + m = r.match(self.private_key) + if not m: + raise ValueError("Not a valid PEM pre boundary") + pem_header = m.group(1) + key = serialization.load_pem_private_key(self.private_key.encode(), None, default_backend()) + if pem_header == 'RSA PRIVATE KEY': + sign = key.sign(data.encode(), padding.PKCS1v15(), hashes.SHA256()) + self.digest_algorithm = 'rsa-sha256' + elif pem_header == 'EC PRIVATE KEY': + sign = key.sign(data.encode(), ec.ECDSA(hashes.SHA256())) + self.digest_algorithm = 'hs2019' + else: + raise Exception("Unsupported key: {0}".format(pem_header)) + + return b64encode(sign) + + def get_auth_header(self, hdrs, signed_msg): + """ + Assmebled an Intersight formatted authorization header + + :param hdrs : object with header keys + :param signed_msg: base64 encoded sha256 hashed body + :return: concatenated authorization header + """ + + auth_str = "Signature" + + auth_str = auth_str + " " + "keyId=\"" + self.public_key + "\"," + "algorithm=\"" + self.digest_algorithm + "\"," + + auth_str = auth_str + "headers=\"(request-target)" + + for key, dummy in hdrs.items(): + auth_str = auth_str + " " + key.lower() + auth_str = auth_str + "\"" + + auth_str = auth_str + "," + "signature=\"" + signed_msg.decode('ascii') + "\"" + + return auth_str + + def get_moid_by_name(self, resource_path, target_name): + """ + Retrieve an Intersight object moid by name + + :param resource_path: intersight resource path e.g. '/ntp/Policies' + :param target_name: intersight object name + :return: json http response object + """ + query_params = { + "$filter": "Name eq '{0}'".format(target_name) + } + + options = { + "http_method": "GET", + "resource_path": resource_path, + "query_params": query_params + } + + get_moid = self.intersight_call(**options) + + if get_moid.json()['Results'] is not None: + located_moid = get_moid.json()['Results'][0]['Moid'] + else: + raise KeyError('Intersight object with name "{0}" not found!'.format(target_name)) + + return located_moid + + def call_api(self, **options): + """ + Call the Intersight API and check for success status + :param options: options dict with method and other params for API call + :return: json http response object + """ + + try: + response, info = self.intersight_call(**options) + if not re.match(r'2..', str(info['status'])): + raise RuntimeError(info['status'], info['msg'], info['body']) + except Exception as e: + self.module.fail_json(msg="API error: %s " % str(e)) + + response_data = response.read() + if len(response_data) > 0: + resp_json = json.loads(response_data) + resp_json['trace_id'] = info.get('x-starship-traceid') + return resp_json + return {} + + def intersight_call(self, http_method="", resource_path="", query_params=None, body=None, moid=None, name=None): + """ + Invoke the Intersight API + + :param resource_path: intersight resource path e.g. '/ntp/Policies' + :param query_params: dictionary object with query string parameters as key/value pairs + :param body: dictionary object with intersight data + :param moid: intersight object moid + :param name: intersight object name + :return: json http response object + """ + + target_host = urlparse(self.host).netloc + target_path = urlparse(self.host).path + query_path = "" + method = http_method.upper() + bodyString = "" + + # Verify an accepted HTTP verb was chosen + if(method not in ['GET', 'POST', 'PATCH', 'DELETE']): + raise ValueError('Please select a valid HTTP verb (GET/POST/PATCH/DELETE)') + + # Verify the resource path isn't empy & is a valid <str> object + if(resource_path != "" and not (resource_path, str)): + raise TypeError('The *resource_path* value is required and must be of type "<str>"') + + # Verify the query parameters isn't empy & is a valid <dict> object + if(query_params is not None and not isinstance(query_params, dict)): + raise TypeError('The *query_params* value must be of type "<dict>"') + + # Verify the MOID is not null & of proper length + if(moid is not None and len(moid.encode('utf-8')) != 24): + raise ValueError('Invalid *moid* value!') + + # Check for query_params, encode, and concatenate onto URL + if query_params: + query_path = "?" + urlencode(query_params) + + # Handle PATCH/DELETE by Object "name" instead of "moid" + if method in ('PATCH', 'DELETE'): + if moid is None: + if name is not None: + if isinstance(name, str): + moid = self.get_moid_by_name(resource_path, name) + else: + raise TypeError('The *name* value must be of type "<str>"') + else: + raise ValueError('Must set either *moid* or *name* with "PATCH/DELETE!"') + + # Check for moid and concatenate onto URL + if moid is not None: + resource_path += "/" + moid + + # Check for GET request to properly form body + if method != "GET": + bodyString = json.dumps(body) + + # Concatenate URLs for headers + target_url = self.host + resource_path + query_path + request_target = method.lower() + " " + target_path + resource_path + query_path + + # Get the current GMT Date/Time + cdate = get_gmt_date() + + # Generate the body digest + body_digest = get_sha256_digest(bodyString) + b64_body_digest = b64encode(body_digest.digest()) + + # Generate the authorization header + auth_header = { + 'Host': target_host, + 'Date': cdate, + 'Digest': "SHA-256=" + b64_body_digest.decode('ascii'), + } + + string_to_sign = prepare_str_to_sign(request_target, auth_header) + b64_signed_msg = self.get_sig_b64encode(string_to_sign) + auth_header = self.get_auth_header(auth_header, b64_signed_msg) + + # Generate the HTTP requests header + request_header = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Host': '{0}'.format(target_host), + 'Date': '{0}'.format(cdate), + 'Digest': 'SHA-256={0}'.format(b64_body_digest.decode('ascii')), + 'Authorization': '{0}'.format(auth_header), + } + + response, info = fetch_url(self.module, target_url, data=bodyString, headers=request_header, method=method, use_proxy=self.module.params['use_proxy']) + + return response, info + + def get_resource(self, resource_path, query_params, return_list=False): + ''' + GET a resource and return the 1st element found or the full Results list + ''' + options = { + 'http_method': 'get', + 'resource_path': resource_path, + 'query_params': query_params, + } + response = self.call_api(**options) + if response.get('Results'): + if return_list: + self.result['api_response'] = response['Results'] + else: + # return the 1st list element + self.result['api_response'] = response['Results'][0] + self.result['trace_id'] = response.get('trace_id') + + def configure_resource(self, moid, resource_path, body, query_params, update_method=''): + if not self.module.check_mode: + if moid and update_method != 'post': + # update the resource - user has to specify all the props they want updated + options = { + 'http_method': 'patch', + 'resource_path': resource_path, + 'body': body, + 'moid': moid, + } + response_dict = self.call_api(**options) + if response_dict.get('Results'): + # return the 1st element in the results list + self.result['api_response'] = response_dict['Results'][0] + self.result['trace_id'] = response_dict.get('trace_id') + else: + # create the resource + options = { + 'http_method': 'post', + 'resource_path': resource_path, + 'body': body, + } + response_dict = self.call_api(**options) + if response_dict: + self.result['api_response'] = response_dict + self.result['trace_id'] = response_dict.get('trace_id') + elif query_params: + # POSTs may not return any data. + # Get the current state of the resource if query_params. + self.get_resource( + resource_path=resource_path, + query_params=query_params, + ) + self.result['changed'] = True + + def delete_resource(self, moid, resource_path): + # delete resource and create empty api_response + if not self.module.check_mode: + options = { + 'http_method': 'delete', + 'resource_path': resource_path, + 'moid': moid, + } + resp = self.call_api(**options) + self.result['api_response'] = {} + self.result['trace_id'] = resp.get('trace_id') + self.result['changed'] = True + + def configure_policy_or_profile(self, resource_path): + # Configure (create, update, or delete) the policy or profile + organization_moid = None + # GET Organization Moid + self.get_resource( + resource_path='/organization/Organizations', + query_params={ + '$filter': "Name eq '" + self.module.params['organization'] + "'", + '$select': 'Moid', + }, + ) + if self.result['api_response'].get('Moid'): + # resource exists and moid was returned + organization_moid = self.result['api_response']['Moid'] + + self.result['api_response'] = {} + # Get the current state of the resource + filter_str = "Name eq '" + self.module.params['name'] + "'" + filter_str += "and Organization.Moid eq '" + organization_moid + "'" + self.get_resource( + resource_path=resource_path, + query_params={ + '$filter': filter_str, + '$expand': 'Organization', + } + ) + + moid = None + resource_values_match = False + if self.result['api_response'].get('Moid'): + # resource exists and moid was returned + moid = self.result['api_response']['Moid'] + if self.module.params['state'] == 'present': + resource_values_match = compare_values(self.api_body, self.result['api_response']) + else: # state == 'absent' + self.delete_resource( + moid=moid, + resource_path=resource_path, + ) + moid = None + + if self.module.params['state'] == 'present' and not resource_values_match: + # remove read-only Organization key + self.api_body.pop('Organization') + if not moid: + # Organization must be set, but can't be changed after initial POST + self.api_body['Organization'] = { + 'Moid': organization_moid, + } + self.configure_resource( + moid=moid, + resource_path=resource_path, + body=self.api_body, + query_params={ + '$filter': filter_str + } + ) + if self.result['api_response'].get('Moid'): + # resource exists and moid was returned + moid = self.result['api_response']['Moid'] + + return moid diff --git a/ansible_collections/cisco/intersight/plugins/modules/intersight_boot_order_policy.py b/ansible_collections/cisco/intersight/plugins/modules/intersight_boot_order_policy.py new file mode 100644 index 00000000..1c46a310 --- /dev/null +++ b/ansible_collections/cisco/intersight/plugins/modules/intersight_boot_order_policy.py @@ -0,0 +1,495 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: intersight_boot_order_policy +short_description: Boot Order policy configuration for Cisco Intersight +description: + - Boot Order policy configuration for Cisco Intersight. + - Used to configure Boot Order servers and timezone settings on Cisco Intersight managed devices. + - For more information see L(Cisco Intersight,https://intersight.com/apidocs). +extends_documentation_fragment: intersight +options: + state: + description: + - If C(present), will verify the resource is present and will create if needed. + - If C(absent), will verify the resource is absent and will delete if needed. + choices: [present, absent] + default: present + organization: + description: + - The name of the Organization this resource is assigned to. + - Profiles and Policies that are created within a Custom Organization are applicable only to devices in the same Organization. + default: default + name: + description: + - The name assigned to the Boot Order policy. + - The name must be between 1 and 62 alphanumeric characters, allowing special characters :-_. + required: true + tags: + description: + - List of tags in Key:<user-defined key> Value:<user-defined value> format. + type: list + description: + description: + - The user-defined description of the Boot Order policy. + - Description can contain letters(a-z, A-Z), numbers(0-9), hyphen(-), period(.), colon(:), or an underscore(_). + aliases: [descr] + configured_boot_mode: + description: + - Sets the BIOS boot mode. + - UEFI uses the GUID Partition Table (GPT) whereas Legacy mode uses the Master Boot Record (MBR) partitioning scheme. + choices: [Legacy, Uefi] + default: Legacy + uefi_enable_secure_boot: + description: + - Secure boot enforces that device boots using only software that is trusted by the Original Equipment Manufacturer (OEM). + - Option is only used if configured_boot_mode is set to Uefi. + type: bool + default: false + boot_devices: + description: + - List of Boot Devices configured on the endpoint. + type: list + suboptions: + enabled: + description: + - Specifies if the boot device is enabled or disabled. + type: bool + default: true + device_type: + description: + - Device type used with this boot option. + - Choices are based on each device title in the API schema. + choices: [iSCSI, Local CDD, Local Disk, NVMe, PCH Storage, PXE, SAN, SD Card, UEFI Shell, USB, Virtual Media] + required: true + device_name: + description: + - A name that helps identify a boot device. + - It can be any string that adheres to the following constraints. + - It should start and end with an alphanumeric character. + - It can have underscores and hyphens. + - It cannot be more than 30 characters. + required: true + network_slot: + description: + - The slot id of the controller for the iscsi and pxe device. + - Option is used when device_type is iscsi and pxe. + choices: [1 - 255, MLOM, L, L1, L2, OCP] + port: + description: + - The port id of the controller for the iscsi and pxe device. + - Option is used when device_type is iscsi and pxe. + - The port id need to be an integer from 0 to 255. + controller_slot: + description: + - The slot id of the controller for the local disk device. + - Option is used when device_type is local_disk. + choices: [1-255, M, HBA, SAS, RAID, MRAID, MSTOR-RAID] + bootloader_name: + description: + - Details of the bootloader to be used during boot from local disk. + - Option is used when device_type is local_disk and configured_boot_mode is Uefi. + bootloader_description: + description: + - Details of the bootloader to be used during boot from local disk. + - Option is used when device_type is local_disk and configured_boot_mode is Uefi. + bootloader_path: + description: + - Details of the bootloader to be used during boot from local disk. + - Option is used when device_type is local_disk and configured_boot_mode is Uefi. + ip_type: + description: + - The IP Address family type to use during the PXE Boot process. + - Option is used when device_type is pxe. + choices: [None, IPv4, IPv6] + default: None + interface_source: + description: + - Lists the supported Interface Source for PXE device. + - Option is used when device_type is pxe. + choices: [name, mac, port] + default: name + intefrace_name: + description: + - The name of the underlying virtual ethernet interface used by the PXE boot device. + - Option is used when device_type is pxe and interface_source is name. + mac_address: + description: + - The MAC Address of the underlying virtual ethernet interface used by the PXE boot device. + - Option is used when device_type is pxe and interface_source is mac. + sd_card_subtype: + description: + - The subtype for the selected device type. + - Option is used when device_type is sd_card. + choices: [None, flex-util, flex-flash, SDCARD] + default: None + lun: + description: + - The Logical Unit Number (LUN) of the device. + - Option is used when device_type is pch, san and sd_card. + - The LUN need to be an integer from 0 to 255. + usb_subtype: + description: + - The subtype for the selected device type. + - Option is used when device_type is usb. + choices: [None, usb-cd, usb-fdd, usb-hdd] + default: None + virtual_media_subtype: + description: + - The subtype for the selected device type. + - Option is used when device_type is virtual_media. + choices: [None, cimc-mapped-dvd, cimc-mapped-hdd, kvm-mapped-dvd, kvm-mapped-hdd, kvm-mapped-fdd] + default: None +author: + - Tse Kai "Kevin" Chan (@BrightScale) +version_added: '2.10' +''' + +EXAMPLES = r''' +- name: Configure Boot Order Policy + cisco.intersight.intersight_boot_order_policy: + api_private_key: "{{ api_private_key }}" + api_key_id: "{{ api_key_id }}" + organization: DevNet + name: COS-Boot + description: Boot Order policy for COS + tags: + - Key: Site + Value: RCDN + configured_boot_mode: legacy + boot_devices: + - device_type: Local Disk + device_name: Boot-Lun + controller_slot: MRAID + +- name: Delete Boot Order Policy + cisco.intersight.intersight_boot_policy: + api_private_key: "{{ api_private_key }}" + api_key_id: "{{ api_key_id }}" + organization: DevNet + name: COS-Boot + state: absent +''' + +RETURN = r''' +api_repsonse: + description: The API response output returned by the specified resource. + returned: always + type: dict + sample: + "api_response": { + "Name": "COS-Boot", + "ObjectType": "boot.Policy", + "Tags": [ + { + "Key": "Site", + "Value": "RCDN" + } + ] + } +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.intersight.plugins.module_utils.intersight import IntersightModule, intersight_argument_spec + + +def main(): + boot_device = dict( + enabled=dict(type='bool', default=True), + device_type=dict( + type='str', + choices=[ + 'iSCSI', + 'Local CDD', + 'Local Disk', + 'NVMe', + 'PCH Storage', + 'PXE', + 'SAN', + 'SD Card', + 'UEFI Shell', + 'USB', + 'Virtual Media', + ], + required=True, + ), + device_name=dict(type='str', required=True), + # iscsi and pxe options + network_slot=dict(type='str', default=''), + port=dict(type='int', default=0), + # local disk options + controller_slot=dict(type='str', default=''), + # bootloader options + bootloader_name=dict(type='str', default=''), + bootloader_description=dict(type='str', default=''), + bootloader_path=dict(type='str', default=''), + # pxe only options + ip_type=dict( + type='str', + choices=[ + 'None', + 'IPv4', + 'IPv6' + ], + default='None' + ), + interface_source=dict( + type='str', + choices=[ + 'name', + 'mac', + 'port' + ], + default='name' + ), + interface_name=dict(type='str', default=''), + mac_address=dict(type='str', defualt=''), + # sd card options + sd_card_subtype=dict( + type='str', + choices=[ + 'None', + 'flex-util', + 'flex-flash', + 'SDCARD' + ], + default='None', + ), + # lun for pch, san, sd_card + lun=dict(type='int', default=0), + # usb options + usb_subtype=dict( + type='str', + choices=[ + 'None', + 'usb-cd', + 'usb-fdd', + 'usb-hdd' + ], + default='None', + ), + # virtual media options + virtual_media_subtype=dict( + type='str', + choices=[ + 'None', + 'cimc-mapped-dvd', + 'cimc-mapped-hdd', + 'kvm-mapped-dvd', + 'kvm-mapped-hdd', + 'kvm-mapped-fdd' + ], + default='None', + ), + ) + argument_spec = intersight_argument_spec + argument_spec.update( + state=dict(type='str', choices=['present', 'absent'], default='present'), + organization=dict(type='str', default='default'), + name=dict(type='str', required=True), + description=dict(type='str', aliases=['descr'], default=''), + tags=dict(type='list', default=[]), + configured_boot_mode=dict(type='str', choices=['Legacy', 'Uefi'], default='Legacy'), + uefi_enable_secure_boot=dict(type='bool', default=False), + boot_devices=dict(type='list', elements='dict', options=boot_device), + ) + + module = AnsibleModule( + argument_spec, + supports_check_mode=True, + ) + + intersight = IntersightModule(module) + intersight.result['api_response'] = {} + intersight.result['trace_id'] = '' + # + # Argument spec above, resource path, and API body should be the only code changed in each policy module + # + # Resource path used to configure policy + resource_path = '/boot/PrecisionPolicies' + # Define API body used in compares or create + intersight.api_body = { + 'Organization': { + 'Name': intersight.module.params['organization'], + }, + 'Name': intersight.module.params['name'], + 'Tags': intersight.module.params['tags'], + 'Description': intersight.module.params['description'], + 'ConfiguredBootMode': intersight.module.params['configured_boot_mode'], + "EnforceUefiSecureBoot": intersight.module.params['uefi_enable_secure_boot'], + 'BootDevices': [], + } + if intersight.module.params.get('boot_devices'): + for device in intersight.module.params['boot_devices']: + if device['device_type'] == 'iSCSI': + intersight.api_body['BootDevices'].append( + { + "ClassId": "boot.Iscsi", + "ObjectType": "boot.Iscsi", + "Enabled": device['enabled'], + "Name": device['device_name'], + "Slot": device['network_slot'], + "Port": device['port'], + } + ) + elif device['device_type'] == 'Local CDD': + intersight.api_body['BootDevices'].append( + { + "ClassId": "boot.LocalCDD", + "ObjectType": "boot.LocalCDD", + "Enabled": device['enabled'], + "Name": device['device_name'], + } + ) + elif device['device_type'] == 'Local Disk': + intersight.api_body['BootDevices'].append( + { + "ClassId": "boot.LocalDisk", + "ObjectType": "boot.LocalDisk", + "Enabled": device['enabled'], + "Name": device['device_name'], + "Slot": device['controller_slot'], + "Bootloader": { + "ClassId": "boot.Bootloader", + "ObjectType": "boot.Bootloader", + "Description": device['bootloader_description'], + "Name": device['bootloader_name'], + "Path": device['bootloader_path'], + }, + } + ) + elif device['device_type'] == 'NVMe': + intersight.api_body['BootDevices'].append( + { + "ClassId": "boot.NVMe", + "ObjectType": "boot.NVMe", + "Enabled": device['enabled'], + "Name": device['device_name'], + "Bootloader": { + "ClassId": "boot.Bootloader", + "ObjectType": "boot.Bootloader", + "Description": device['bootloader_description'], + "Name": device['bootloader_name'], + "Path": device['bootloader_path'], + }, + } + ) + elif device['device_type'] == 'PCH Storage': + intersight.api_body['BootDevices'].append( + { + "ClassId": "boot.PchStorage", + "ObjectType": "boot.PchStorage", + "Enabled": device['enabled'], + "Name": device['device_name'], + "Bootloader": { + "ClassId": "boot.Bootloader", + "ObjectType": "boot.Bootloader", + "Description": device['bootloader_description'], + "Name": device['bootloader_name'], + "Path": device['bootloader_path'], + }, + "Lun": device['lun'], + } + ) + elif device['device_type'] == 'PXE': + intersight.api_body['BootDevices'].append( + { + "ClassId": "boot.Pxe", + "ObjectType": "boot.Pxe", + "Enabled": device['enabled'], + "Name": device['device_name'], + "IpType": device['ip_type'], + "InterfaceSource": device['interface_source'], + "Slot": device['network_slot'], + "InterfaceName": device['interface_name'], + "Port": device['port'], + "MacAddress": device['mac_address'], + } + ) + elif device['device_type'] == 'SAN': + intersight.api_body['BootDevices'].append( + { + "ClassId": "boot.San", + "ObjectType": "boot.San", + "Enabled": device['enabled'], + "Name": device['device_name'], + "Lun": device['lun'], + "Slot": device['network_slot'], + "Bootloader": { + "ClassId": "boot.Bootloader", + "ObjectType": "boot.Bootloader", + "Description": device['bootloader_description'], + "Name": device['bootloader_name'], + "Path": device['bootloader_path'], + }, + } + ) + elif device['device_type'] == 'SD Card': + intersight.api_body['BootDevices'].append( + { + "ClassId": "boot.SdCard", + "ObjectType": "boot.SdCard", + "Enabled": device['enabled'], + "Name": device['device_name'], + "Lun": device['lun'], + "SubType": device['sd_card_subtype'], + "Bootloader": { + "ClassId": "boot.Bootloader", + "ObjectType": "boot.Bootloader", + "Description": device['bootloader_description'], + "Name": device['bootloader_name'], + "Path": device['bootloader_path'], + }, + } + ) + elif device['device_type'] == 'UEFI Shell': + intersight.api_body['BootDevices'].append( + { + "ClassId": "boot.UefiShell", + "ObjectType": "boot.UefiShell", + "Enabled": device['enabled'], + "Name": device['device_name'], + } + ) + elif device['device_type'] == 'USB': + intersight.api_body['BootDevices'].append( + { + "ClassId": "boot.Usb", + "ObjectType": "boot.Usb", + "Enabled": device['enabled'], + "Name": device['device_name'], + "SubType": device['usb_subtype'], + } + ) + elif device['device_type'] == 'Virtual Media': + intersight.api_body['BootDevices'].append( + { + "ClassId": "boot.VirtualMedia", + "ObjectType": "boot.VirtualMedia", + "Enabled": device['enabled'], + "Name": device['device_name'], + "SubType": device['virtual_media_subtype'], + } + ) + # + # Code below should be common across all policy modules + # + intersight.configure_policy_or_profile(resource_path=resource_path) + + module.exit_json(**intersight.result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/cisco/intersight/plugins/modules/intersight_imc_access_policy.py b/ansible_collections/cisco/intersight/plugins/modules/intersight_imc_access_policy.py new file mode 100644 index 00000000..ec31898e --- /dev/null +++ b/ansible_collections/cisco/intersight/plugins/modules/intersight_imc_access_policy.py @@ -0,0 +1,200 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: intersight_imc_access_policy +short_description: IMC Access Policy configuration for Cisco Intersight +description: + - IMC Access Policy configuration for Cisco Intersight. + - Used to configure IP addresses and VLAN used for external connectivity to Cisco IMC. + - For more information see L(Cisco Intersight,https://intersight.com/apidocs). +extends_documentation_fragment: intersight +options: + state: + description: + - If C(present), will verify the resource is present and will create if needed. + - If C(absent), will verify the resource is absent and will delete if needed. + choices: [present, absent] + default: present + organization: + description: + - The name of the Organization this resource is assigned to. + - Profiles and Policies that are created within a Custom Organization are applicable only to devices in the same Organization. + default: default + name: + description: + - The name assigned to the IMC Access Policy. + - The name must be between 1 and 62 alphanumeric characters, allowing special characters :-_. + required: true + tags: + description: + - List of tags in Key:<user-defined key> Value:<user-defined value> format. + descrption: + description: + - The user-defined description of the IMC access policy. + - Description can contain letters(a-z, A-Z), numbers(0-9), hyphen(-), period(.), colon(:), or an underscore(_). + aliases: [descr] + vlan_id: + description: + - VLAN to be used for server access over Inband network. + required: true + type: int + ip_pool: + description: + - IP Pool used to assign IP address and other required network settings. + required: true +author: + - David Soper (@dsoper2) +version_added: '2.10' +''' + +EXAMPLES = r''' +- name: Configure IMC Access policy + intersight_imc_access_policy: + api_private_key: "{{ api_private_key }}" + api_key_id: "{{ api_key_id }}" + name: sjc02-d23-access + description: IMC access for SJC02 rack D23 + tags: + - Site: D23 + vlan_id: 131 + ip_pool: sjc02-d23-ext-mgmt + +- name: Delete IMC Access policy + intersight_imc_access_policy: + api_private_key: "{{ api_private_key }}" + api_key_id: "{{ api_key_id }}" + name: sjc02-d23-access + state: absent +''' + +RETURN = r''' +api_repsonse: + description: The API response output returned by the specified resource. + returned: always + type: dict + sample: + "api_response": { + "Name": "sjc02-d23-access", + "ObjectType": "access.Policy", + "Profiles": [ + { + "Moid": "5e4ec7ae77696e2d30840cfc", + "ObjectType": "server.Profile", + }, + { + "Moid": "5e84d78777696e2d302ec195", + "ObjectType": "server.Profile", + } + ], + "Tags": [ + { + "Key": "Site", + "Value": "SJC02" + } + ] + } +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.intersight.plugins.module_utils.intersight import IntersightModule, intersight_argument_spec, compare_values + + +def main(): + argument_spec = intersight_argument_spec + argument_spec.update( + state=dict(type='str', choices=['present', 'absent'], default='present'), + organization=dict(type='str', default='default'), + name=dict(type='str', required=True), + description=dict(type='str', aliases=['descr'], default=''), + tags=dict(type='list', default=[]), + vlan_id=dict(type='int', required=True), + ip_pool=dict(type='str', required=True), + ) + + module = AnsibleModule( + argument_spec, + supports_check_mode=True, + ) + + intersight = IntersightModule(module) + intersight.result['api_response'] = {} + intersight.result['trace_id'] = '' + intersight.api_body = { + 'Name': intersight.module.params['name'], + 'Tags': intersight.module.params['tags'], + 'Description': intersight.module.params['description'], + 'InbandVlan': intersight.module.params['vlan_id'], + 'Organization': { + 'Name': intersight.module.params['organization'], + }, + } + + # get the current state of the resource + intersight.get_resource( + resource_path='/access/Policies', + query_params={ + '$filter': "Name eq '" + intersight.module.params['name'] + "'", + '$expand': 'Organization', + }, + ) + + moid = None + resource_values_match = False + if intersight.result['api_response'].get('Moid'): + # resource exists and moid was returned + moid = intersight.result['api_response']['Moid'] + if module.params['state'] == 'present': + resource_values_match = compare_values(intersight.api_body, intersight.result['api_response']) + else: # state == 'absent' + intersight.delete_resource( + moid=moid, + resource_path='/access/Policies', + ) + moid = None + + if module.params['state'] == 'present' and not resource_values_match: + # remove read-only Organization key + intersight.api_body.pop('Organization') + if not moid: + # GET Organization Moid + intersight.get_resource( + resource_path='/organization/Organizations', + query_params={ + '$filter': "Name eq '" + intersight.module.params['organization'] + "'", + '$select': 'Moid', + }, + ) + organization_moid = None + if intersight.result['api_response'].get('Moid'): + # resource exists and moid was returned + organization_moid = intersight.result['api_response']['Moid'] + # Organization must be set, but can't be changed after initial POST + intersight.api_body['Organization'] = { + 'Moid': organization_moid, + } + intersight.configure_resource( + moid=moid, + resource_path='/access/Policies', + body=intersight.api_body, + query_params={ + '$filter': "Name eq '" + intersight.module.params['name'] + "'", + }, + ) + + module.exit_json(**intersight.result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/cisco/intersight/plugins/modules/intersight_info.py b/ansible_collections/cisco/intersight/plugins/modules/intersight_info.py new file mode 100644 index 00000000..ba21df24 --- /dev/null +++ b/ansible_collections/cisco/intersight/plugins/modules/intersight_info.py @@ -0,0 +1,117 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: intersight_info +short_description: Gather information about Intersight +description: +- Gathers information about servers in L(Cisco Intersight,https://intersight.com). +- This module was called C(intersight_facts) before Ansible 2.9. The usage did not change. +extends_documentation_fragment: intersight +options: + server_names: + description: + - Server names to retrieve information from. + - An empty list will return all servers. + type: list + required: yes +author: +- David Soper (@dsoper2) +- CiscoUcs (@CiscoUcs) +version_added: '2.8' +''' + +EXAMPLES = r''' +- name: Get info for all servers + intersight_info: + api_private_key: ~/Downloads/SecretKey.txt + api_key_id: 64612d300d0982/64612d300d0b00/64612d300d3650 + server_names: +- debug: + msg: "server name {{ item.Name }}, moid {{ item.Moid }}" + loop: "{{ intersight_servers }}" + when: intersight_servers is defined + +- name: Get info for servers by name + intersight_info: + api_private_key: ~/Downloads/SecretKey.txt + api_key_id: 64612d300d0982/64612d300d0b00/64612d300d3650 + server_names: + - SJC18-L14-UCS1-1 +- debug: + msg: "server moid {{ intersight_servers[0].Moid }}" + when: intersight_servers[0] is defined +''' + +RETURN = r''' +intersight_servers: + description: A list of Intersight Servers. See L(Cisco Intersight,https://intersight.com/apidocs) for details. + returned: always + type: complex + contains: + Name: + description: The name of the server. + returned: always + type: str + sample: SJC18-L14-UCS1-1 + Moid: + description: The unique identifier of this Managed Object instance. + returned: always + type: str + sample: 5978bea36ad4b000018d63dc +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.intersight.plugins.module_utils.intersight import IntersightModule, intersight_argument_spec + + +def get_servers(module, intersight): + query_list = [] + if module.params['server_names']: + for server in module.params['server_names']: + query_list.append("Name eq '%s'" % server) + query_str = ' or '.join(query_list) + options = { + 'http_method': 'get', + 'resource_path': '/compute/PhysicalSummaries', + 'query_params': { + '$filter': query_str, + '$top': 1000, + } + } + response_dict = intersight.call_api(**options) + + return response_dict.get('Results') + + +def main(): + argument_spec = intersight_argument_spec + argument_spec.update( + server_names=dict(type='list', required=True), + ) + + module = AnsibleModule( + argument_spec, + supports_check_mode=True, + ) + if module._name == 'intersight_facts': + module.deprecate("The 'intersight_facts' module has been renamed to 'intersight_info'", version='2.13') + + intersight = IntersightModule(module) + + # one API call returning all requested servers + module.exit_json(intersight_servers=get_servers(module, intersight)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/cisco/intersight/plugins/modules/intersight_local_user_policy.py b/ansible_collections/cisco/intersight/plugins/modules/intersight_local_user_policy.py new file mode 100644 index 00000000..f9f8a971 --- /dev/null +++ b/ansible_collections/cisco/intersight/plugins/modules/intersight_local_user_policy.py @@ -0,0 +1,363 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: intersight_local_user_policy +short_description: Local User Policy configuration for Cisco Intersight +description: + - Local User Policy configuration for Cisco Intersight. + - Used to configure local users on endpoint devices. + - For more information see L(Cisco Intersight,https://intersight.com/apidocs). +extends_documentation_fragment: intersight +options: + state: + description: + - If C(present), will verify the resource is present and will create if needed. + - If C(absent), will verify the resource is absent and will delete if needed. + choices: [present, absent] + default: present + organization: + description: + - The name of the Organization this resource is assigned to. + - Profiles and Policies that are created within a Custom Organization are applicable only to devices in the same Organization. + default: default + name: + description: + - The name assigned to the Local User Policy. + - The name must be between 1 and 62 alphanumeric characters, allowing special characters :-_. + required: true + tags: + description: + - List of tags in Key:<user-defined key> Value:<user-defined value> format. + description: + description: + - The user-defined description of the Local User policy. + - Description can contain letters(a-z, A-Z), numbers(0-9), hyphen(-), period(.), colon(:), or an underscore(_). + aliases: [descr] + enforce_strong_password: + description: + - If true, enables a strong password policy. + - Strong password requirements:. + - A. The password must have a minimum of 8 and a maximum of 20 characters. + - B. The password must not contain the User's Name. + - C. The password must contain characters from three of the following four categories. + - 1) English uppercase characters (A through Z). + - 2) English lowercase characters (a through z). + - 3) Base 10 digits (0 through 9). + - 4) Non-alphabetic characters (! , @, '#', $, %, ^, &, *, -, _, +, =). + type: bool + default: true + enable_password_expiry: + description: + - Enables password expiry on the endpoint. + type: bool + default: false + password_history: + description: + - Specifies number of times a password cannot repeat when changed (value between 0 and 5). + - Entering 0 disables this option. + type: int + default: 5 + local_users: + description: + - List of local users on the endpoint. + - An admin user already exists on the endpoint. + - Add the admin user here only if you want to change the password, or enable or disable the user. + - To add admin user, provide a username as 'admin', select the admin user role, and then proceed. + suboptions: + username: + description: + - Name of the user created on the endpoint. + required: true + enable: + description: + - Enable or disable the user. + type: bool + default: true + role: + description: + - Roles associated with the user on the endpoint. + choices: [admin, readonly, user] + required: true + password: + description: + - Valid login password of the user. + required: true + purge: + description: + - The purge argument instructs the module to consider the resource definition absolute. + - If true, any previously configured usernames will be removed from the policy with the exception of the `admin` user which cannot be deleted. + default: false + always_update_password: + description: + - Since passwords are not returned by the API and are encrypted on the endpoint, this option will instruct the module when to change the password. + - If true, the password for each user will always be updated in the policy. + - If false, the password will be updated only if the user is created. + default: false +author: + - David Soper (@dsoper2) +version_added: '2.10' +''' + +EXAMPLES = r''' +- name: Configure Local User policy + intersight_local_user_policy: + api_private_key: "{{ api_private_key }}" + api_key_id: "{{ api_key_id }}" + name: guest-admin + tags: + - Key: username + Value: guest + description: User named guest with admin role + local_users: + - username: guest + role: admin + password: vault_guest_password + - username: reader + role: readonly + password: vault_reader_password + +- name: Delete Local User policy + intersight_local_user_policy: + api_private_key: "{{ api_private_key }}" + api_key_id: "{{ api_key_id }}" + name: guest-admin + state: absent +''' + +RETURN = r''' +api_repsonse: + description: The API response output returned by the specified resource. + returned: always + type: dict + sample: + "api_response": { + "Description": "User named guest with admin role", + "EndPointUserRoles": [ + { + "ChangePassword": true, + "Enabled": true + } + ] + } +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.intersight.plugins.module_utils.intersight import IntersightModule, intersight_argument_spec, compare_values + + +def main(): + local_user = dict( + username=dict(type='str', required=True), + enable=dict(type='bool', default=True), + role=dict(type='str', choices=['admin', 'readonly', 'user'], required=True), + password=dict(type='str', required=True, no_log=True), + ) + argument_spec = intersight_argument_spec + argument_spec.update( + state=dict(type='str', choices=['present', 'absent'], default='present'), + organization=dict(type='str', default='default'), + name=dict(type='str', required=True), + description=dict(type='str', aliases=['descr'], default=''), + tags=dict(type='list', default=[]), + enforce_strong_password=dict(type='bool', default=True, no_log=False), + enable_password_expiry=dict(type='bool', default=False, no_log=False), + password_history=dict(type='int', default=5, no_log=False), + local_users=dict(type='list', elements='dict', options=local_user, default=[]), + purge=dict(type='bool', default=False), + always_update_password=dict(type='bool', default=False, no_log=False), + ) + + module = AnsibleModule( + argument_spec, + supports_check_mode=True, + ) + + intersight = IntersightModule(module) + intersight.result['api_response'] = {} + intersight.result['trace_id'] = '' + + # get the current state of the resource + intersight.get_resource( + resource_path='/iam/EndPointUserPolicies', + query_params={ + '$filter': "Name eq '" + intersight.module.params['name'] + "'", + '$expand': 'EndPointUserRoles($expand=EndPointRole,EndPointUser),Organization', + }, + ) + + user_policy_moid = None + resource_values_match = False + if intersight.result['api_response'].get('Moid'): + # resource exists and moid was returned + user_policy_moid = intersight.result['api_response']['Moid'] + # + # always_update_password + # false: compare expected vs. actual (won't check passwords) + # true: no compare + # + if module.params['state'] == 'present' and not module.params['always_update_password']: + # Create api body used to check current state + end_point_user_roles = [] + for user in intersight.module.params['local_users']: + end_point_user_roles.append( + { + 'Enabled': user['enable'], + 'EndPointRole': [ + { + 'Name': user['role'], + 'Type': 'IMC', + }, + ], + 'EndPointUser': { + 'Name': user['username'], + }, + } + ) + intersight.api_body = { + 'Name': intersight.module.params['name'], + 'Tags': intersight.module.params['tags'], + 'Description': intersight.module.params['description'], + 'PasswordProperties': { + 'EnforceStrongPassword': intersight.module.params['enforce_strong_password'], + 'EnablePasswordExpiry': intersight.module.params['enable_password_expiry'], + 'PasswordHistory': intersight.module.params['password_history'], + }, + 'EndPointUserRoles': end_point_user_roles, + 'Organization': { + 'Name': intersight.module.params['organization'], + }, + } + resource_values_match = compare_values(intersight.api_body, intersight.result['api_response']) + elif module.params['state'] == 'absent': + intersight.delete_resource( + moid=user_policy_moid, + resource_path='/iam/EndPointUserPolicies', + ) + user_policy_moid = None + + if module.params['state'] == 'present' and not resource_values_match: + intersight.api_body = { + 'Name': intersight.module.params['name'], + 'Tags': intersight.module.params['tags'], + 'Description': intersight.module.params['description'], + 'PasswordProperties': { + 'EnforceStrongPassword': intersight.module.params['enforce_strong_password'], + 'EnablePasswordExpiry': intersight.module.params['enable_password_expiry'], + 'PasswordHistory': intersight.module.params['password_history'], + }, + } + organization_moid = None + if not user_policy_moid or module.params['purge']: + # get Organization Moid which is needed when resources are created + saved_response = intersight.result['api_response'] + intersight.get_resource( + resource_path='/organization/Organizations', + query_params={ + '$filter': "Name eq '" + intersight.module.params['organization'] + "'", + '$select': 'Moid', + }, + ) + if intersight.result['api_response'].get('Moid'): + # resource exists and moid was returned + organization_moid = intersight.result['api_response']['Moid'] + intersight.result['api_response'] = saved_response + if not user_policy_moid: + # Initial create: Organization must be set, but can't be changed after initial POST + intersight.api_body['Organization'] = { + 'Moid': organization_moid, + } + elif module.params['purge']: + # update existing resource and purge any existing users + for end_point_user_role in intersight.result['api_response']['EndPointUserRoles']: + intersight.delete_resource( + moid=end_point_user_role['Moid'], + resource_path='/iam/EndPointUserRoles', + ) + # configure the top-level policy resource + intersight.result['api_response'] = {} + intersight.configure_resource( + moid=user_policy_moid, + resource_path='/iam/EndPointUserPolicies', + body=intersight.api_body, + query_params={ + '$filter': "Name eq '" + intersight.module.params['name'] + "'", + }, + ) + if intersight.result['api_response'].get('Moid'): + # resource exists and moid was returned + user_policy_moid = intersight.result['api_response']['Moid'] + + # EndPointUser local_users list config + for user in intersight.module.params['local_users']: + intersight.api_body = { + 'Name': user['username'], + } + if organization_moid: + intersight.api_body['Organization'] = { + 'Moid': organization_moid, + } + intersight.configure_resource( + moid=None, + resource_path='/iam/EndPointUsers', + body=intersight.api_body, + query_params={ + '$filter': "Name eq '" + user['username'] + "'", + }, + ) + user_moid = None + if intersight.result['api_response'].get('Moid'): + # resource exists and moid was returned + user_moid = intersight.result['api_response']['Moid'] + # GET EndPointRole Moid + intersight.get_resource( + resource_path='/iam/EndPointRoles', + query_params={ + '$filter': "Name eq '" + user['role'] + "' and Type eq 'IMC'", + }, + ) + end_point_role_moid = None + if intersight.result['api_response'].get('Moid'): + # resource exists and moid was returned + end_point_role_moid = intersight.result['api_response']['Moid'] + # EndPointUserRole config + intersight.api_body = { + 'EndPointUser': { + 'Moid': user_moid, + }, + 'EndPointRole': [ + { + 'Moid': end_point_role_moid, + } + ], + 'Password': user['password'], + 'Enabled': user['enable'], + 'EndPointUserPolicy': { + 'Moid': user_policy_moid, + }, + } + intersight.configure_resource( + moid=None, + resource_path='/iam/EndPointUserRoles', + body=intersight.api_body, + query_params={ + '$filter': "EndPointUserPolicy.Moid eq '" + user_policy_moid + "'", + }, + ) + + module.exit_json(**intersight.result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/cisco/intersight/plugins/modules/intersight_ntp_policy.py b/ansible_collections/cisco/intersight/plugins/modules/intersight_ntp_policy.py new file mode 100644 index 00000000..f43eb3a6 --- /dev/null +++ b/ansible_collections/cisco/intersight/plugins/modules/intersight_ntp_policy.py @@ -0,0 +1,160 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: intersight_ntp_policy +short_description: NTP policy configuration for Cisco Intersight +description: + - NTP policy configuration for Cisco Intersight. + - Used to configure NTP servers and timezone settings on Cisco Intersight managed devices. + - For more information see L(Cisco Intersight,https://intersight.com/apidocs). +extends_documentation_fragment: intersight +options: + state: + description: + - If C(present), will verify the resource is present and will create if needed. + - If C(absent), will verify the resource is absent and will delete if needed. + choices: [present, absent] + default: present + organization: + description: + - The name of the Organization this resource is assigned to. + - Profiles and Policies that are created within a Custom Organization are applicable only to devices in the same Organization. + default: default + name: + description: + - The name assigned to the NTP policy. + - The name must be between 1 and 62 alphanumeric characters, allowing special characters :-_. + required: true + tags: + description: + - List of tags in Key:<user-defined key> Value:<user-defined value> format. + type: list + description: + description: + - The user-defined description of the NTP policy. + - Description can contain letters(a-z, A-Z), numbers(0-9), hyphen(-), period(.), colon(:), or an underscore(_). + aliases: [descr] + enable: + description: + - Enable or disable NTP. + type: bool + default: true + ntp_servers: + description: + - List of NTP servers configured on the endpoint. + type: list + timezone: + description: + - Timezone of services on the endpoint. +author: + - David Soper (@dsoper2) +version_added: '2.10' +''' + +EXAMPLES = r''' +- name: Configure NTP Policy + cisco.intersight.intersight_ntp_policy: + api_private_key: "{{ api_private_key }}" + api_key_id: "{{ api_key_id }}" + organization: DevNet + name: lab-ntp + description: NTP policy for lab use + tags: + - Key: Site + Value: RCDN + ntp_servers: + - ntp.esl.cisco.com + timezone: America/Chicago + +- name: Delete NTP Policy + cisco.intersight.intersight_ntp_policy: + api_private_key: "{{ api_private_key }}" + api_key_id: "{{ api_key_id }}" + organization: DevNet + name: lab-ntp + state: absent +''' + +RETURN = r''' +api_repsonse: + description: The API response output returned by the specified resource. + returned: always + type: dict + sample: + "api_response": { + "Name": "lab-ntp", + "ObjectType": "ntp.Policy", + "Tags": [ + { + "Key": "Site", + "Value": "RCDN" + } + ] + } +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.intersight.plugins.module_utils.intersight import IntersightModule, intersight_argument_spec + + +def main(): + argument_spec = intersight_argument_spec + argument_spec.update( + state=dict(type='str', choices=['present', 'absent'], default='present'), + organization=dict(type='str', default='default'), + name=dict(type='str', required=True), + description=dict(type='str', aliases=['descr'], default=''), + tags=dict(type='list', default=[]), + enable=dict(type='bool', default=True), + ntp_servers=dict(type='list', default=[]), + timezone=dict(type='str', default=''), + ) + + module = AnsibleModule( + argument_spec, + supports_check_mode=True, + ) + + intersight = IntersightModule(module) + intersight.result['api_response'] = {} + intersight.result['trace_id'] = '' + # + # Argument spec above, resource path, and API body should be the only code changed in each policy module + # + # Resource path used to configure policy + resource_path = '/ntp/Policies' + # Define API body used in compares or create + intersight.api_body = { + 'Organization': { + 'Name': intersight.module.params['organization'], + }, + 'Name': intersight.module.params['name'], + 'Tags': intersight.module.params['tags'], + 'Description': intersight.module.params['description'], + 'Enabled': intersight.module.params['enable'], + 'NtpServers': intersight.module.params['ntp_servers'], + 'Timezone': intersight.module.params['timezone'], + } + + # + # Code below should be common across all policy modules + # + intersight.configure_policy_or_profile(resource_path=resource_path) + + module.exit_json(**intersight.result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/cisco/intersight/plugins/modules/intersight_rest_api.py b/ansible_collections/cisco/intersight/plugins/modules/intersight_rest_api.py new file mode 100644 index 00000000..5696ace3 --- /dev/null +++ b/ansible_collections/cisco/intersight/plugins/modules/intersight_rest_api.py @@ -0,0 +1,216 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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 +from ansible_collections.cisco.intersight.plugins.module_utils.intersight import IntersightModule, intersight_argument_spec, compare_values +from ansible.module_utils.basic import AnsibleModule +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: intersight_rest_api +short_description: REST API configuration for Cisco Intersight +description: +- Direct REST API configuration for Cisco Intersight. +- All REST API resources and properties must be specified. +- For more information see L(Cisco Intersight,https://intersight.com/apidocs). +extends_documentation_fragment: intersight +options: + resource_path: + description: + - Resource URI being configured related to api_uri. + type: str + required: yes + query_params: + description: + - Query parameters for the Intersight API query languange. + type: dict + update_method: + description: + - The HTTP method used for update operations. + - Some Intersight resources require POST operations for modifications. + type: str + choices: [ patch, post ] + default: patch + api_body: + description: + - The paylod for API requests used to modify resources. + type: dict + list_body: + description: + - The paylod for API requests used to modify resources. + - Should be used instead of api_body if a list is required in the API payload. + type: list + return_list: + description: + - If C(yes), will return a list of API results in the api_response. + - By default only the 1st element of the API Results list is returned. + - Can only be used with GET operations. + type: bool + default: no + state: + description: + - If C(present), will verify the resource is present and will create if needed. + - If C(absent), will verify the resource is absent and will delete if needed. + choices: [present, absent] + default: present +author: +- David Soper (@dsoper2) +- CiscoUcs (@CiscoUcs) +version_added: '2.8' +''' + +EXAMPLES = r''' +- name: Configure Boot Policy + intersight_rest_api: + api_private_key: "{{ api_private_key }}" + api_key_id: "{{ api_key_id }}" + api_key_uri: "{{ api_key_uri }}" + validate_certs: "{{ validate_certs }}" + resource_path: /boot/PrecisionPolicies + query_params: + $filter: "Name eq 'vmedia-localdisk'" + api_body: { + "Name": "vmedia-localdisk", + "ConfiguredBootMode": "Legacy", + "BootDevices": [ + { + "ObjectType": "boot.VirtualMedia", + "Enabled": true, + "Name": "remote-vmedia", + "Subtype": "cimc-mapped-dvd" + }, + { + "ObjectType": "boot.LocalDisk", + "Enabled": true, + "Name": "localdisk", + "Slot": "MRAID", + "Bootloader": null + } + ], + } + state: present + +- name: Delete Boot Policy + intersight_rest_api: + api_private_key: "{{ api_private_key }}" + api_key_id: "{{ api_key_id }}" + api_key_uri: "{{ api_key_uri }}" + validate_certs: "{{ validate_certs }}" + resource_path: /boot/PrecisionPolicies + query_params: + $filter: "Name eq 'vmedia-localdisk'" + state: absent +''' + +RETURN = r''' +api_repsonse: + description: The API response output returned by the specified resource. + returned: always + type: dict + sample: + "api_response": { + "BootDevices": [ + { + "Enabled": true, + "Name": "remote-vmedia", + "ObjectType": "boot.VirtualMedia", + "Subtype": "cimc-mapped-dvd" + }, + { + "Bootloader": null, + "Enabled": true, + "Name": "boot-lun", + "ObjectType": "boot.LocalDisk", + "Slot": "MRAID" + } + ], + "ConfiguredBootMode": "Legacy", + "Name": "vmedia-localdisk", + "ObjectType": "boot.PrecisionPolicy", + } +''' + + +def main(): + argument_spec = intersight_argument_spec + argument_spec.update( + resource_path=dict(type='str', required=True), + query_params=dict(type='dict', default={}), + update_method=dict(type='str', choices=['patch', 'post'], default='patch'), + api_body=dict(type='dict', default={}), + list_body=dict(type='list', default=[]), + return_list=dict(type='bool', default=False), + state=dict(type='str', choices=['absent', 'present'], default='present'), + ) + + module = AnsibleModule( + argument_spec, + supports_check_mode=True, + mutually_exclusive=[ + ['return_list', 'api_body'], + ['return_list', 'state'], + ['api_body', 'list_body'], + ], + ) + + intersight = IntersightModule(module) + intersight.result['api_response'] = {} + intersight.result['trace_id'] = '' + + if module.params['list_body']: + module.params['api_body'] = module.params['list_body'] + + if module.params['update_method'] != 'post' or module.params['query_params']: + # get the current state of the resource + # skip if this is a post to /asset/DeviceClaims or similar resource without GET + intersight.get_resource( + resource_path=module.params['resource_path'], + query_params=module.params['query_params'], + return_list=module.params['return_list'], + ) + + # determine requested operation (config, delete, or neither (get resource only)) + if module.params['state'] == 'present': + request_delete = False + # api_body implies resource configuration through post/patch + request_config = bool(module.params['api_body']) + else: # state == 'absent' + request_delete = True + request_config = False + + moid = None + resource_values_match = False + if (request_config or request_delete) and intersight.result['api_response'].get('Moid'): + # resource exists and moid was returned + moid = intersight.result['api_response']['Moid'] + if request_config: + resource_values_match = compare_values(module.params['api_body'], intersight.result['api_response']) + else: # request_delete + intersight.delete_resource( + moid=moid, + resource_path=module.params['resource_path'], + ) + + if request_config and not resource_values_match: + intersight.configure_resource( + moid=moid, + resource_path=module.params['resource_path'], + body=module.params['api_body'], + query_params=module.params['query_params'], + update_method=module.params['update_method'], + ) + if module.params['return_list'] and not isinstance(intersight.result['api_response'], list): + intersight.result['api_response'] = [] + + module.exit_json(**intersight.result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/cisco/intersight/plugins/modules/intersight_server_profile.py b/ansible_collections/cisco/intersight/plugins/modules/intersight_server_profile.py new file mode 100644 index 00000000..a8cdbccb --- /dev/null +++ b/ansible_collections/cisco/intersight/plugins/modules/intersight_server_profile.py @@ -0,0 +1,282 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: intersight_server_profile +short_description: Server Profile configuration for Cisco Intersight +description: + - Server Profile configuration for Cisco Intersight. + - Used to configure Server Profiles with assigned servers and server policies. + - For more information see L(Cisco Intersight,https://intersight.com/apidocs). +extends_documentation_fragment: intersight +options: + state: + description: + - If C(present), will verify the resource is present and will create if needed. + - If C(absent), will verify the resource is absent and will delete if needed. + choices: [present, absent] + default: present + organization: + description: + - The name of the Organization this resource is assigned to. + - Profiles and Policies that are created within a Custom Organization are applicable only to devices in the same Organization. + default: default + name: + description: + - The name assigned to the Server Profile. + - The name must be between 1 and 62 alphanumeric characters, allowing special characters :-_. + required: true + target_platform: + description: + - The platform for which the server profile is applicable. + - Can either be a server that is operating in Standalone mode or which is attached to a Fabric Interconnect (FIAttached) managed by Intersight. + choices: [Standalone, FIAttached] + default: Standalone + tags: + description: + - List of tags in Key:<user-defined key> Value:<user-defined value> format. + description: + description: + - The user-defined description of the Server Profile. + - Description can contain letters(a-z, A-Z), numbers(0-9), hyphen(-), period(.), colon(:), or an underscore(_). + aliases: [descr] + assigned_server: + description: + - Managed Obect ID (MOID) of assigned server. + - Option can be omitted if user wishes to assign server later. + boot_order_policy: + description: + - Name of Boot Order Policy to associate with this profile. + imc_access_policy: + description: + - Name of IMC Access Policy to associate with this profile. + lan_connectivity_policy: + description: + - Name of LAN Connectivity Policy to associate with this profile. + local_user_policy: + description: + - Name of Local User Policy to associate with this profile. + ntp_policy: + description: + - Name of NTP Policy to associate with this profile. + storage_policy: + description: + - Name of Storage Policy to associate with this profile. + virtual_media_policy: + description: + - Name of Virtual Media Policy to associate with this profile. +author: + - David Soper (@dsoper2) + - Sid Nath (@SidNath21) + - Tse Kai "Kevin" Chan (@BrightScale) + - Soma Tummala (@SOMATUMMALA21) +version_added: '2.10' +''' + +EXAMPLES = r''' +- name: Configure Server Profile + cisco.intersight.intersight_server_profile: + api_private_key: "{{ api_private_key }}" + api_key_id: "{{ api_key_id }}" + name: SP-Server1 + target_platform: FIAttached + tags: + - Key: Site + Value: SJC02 + description: Profile for Server1 + assigned_server: 5e3b517d6176752d319a9999 + boot_order_policy: COS-Boot + imc_access_policy: sjc02-d23-access + lan_connectivity_policy: sjc02-d23-lan + local_user_policy: guest-admin + ntp_policy: lab-ntp + storage_policy: storage + virtual_media_policy: COS-VM + +- name: Delete Server Profile + cisco.intersight.intersight_server_profile: + api_private_key: "{{ api_private_key }}" + api_key_id: "{{ api_key_id }}" + name: SP-Server1 + state: absent +''' + +RETURN = r''' +api_repsonse: + description: The API response output returned by the specified resource. + returned: always + type: dict + sample: + "api_response": { + "AssignedServer": { + "Moid": "5e3b517d6176752d319a0881", + "ObjectType": "compute.Blade", + }, + "Name": "SP-IMM-6454-D23-1-1", + "ObjectType": "server.Profile", + "Tags": [ + { + "Key": "Site", + "Value": "SJC02" + } + ], + "TargetPlatform": "FIAttached", + "Type": "instance" + } +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.intersight.plugins.module_utils.intersight import IntersightModule, intersight_argument_spec + + +def post_profile_to_policy(intersight, moid, resource_path, policy_name): + options = { + 'http_method': 'get', + 'resource_path': resource_path, + 'query_params': { + '$filter': "Name eq '" + policy_name + "'", + }, + } + response = intersight.call_api(**options) + if response.get('Results'): + # get expected policy moid from 1st list element + expected_policy_moid = response['Results'][0]['Moid'] + actual_policy_moid = '' + # check any current profiles and delete if needed + options = { + 'http_method': 'get', + 'resource_path': resource_path, + 'query_params': { + '$filter': "Profiles/any(t: t/Moid eq '" + moid + "')", + }, + } + response = intersight.call_api(**options) + if response.get('Results'): + # get actual moid from 1st list element + actual_policy_moid = response['Results'][0]['Moid'] + if actual_policy_moid != expected_policy_moid: + if not intersight.module.check_mode: + # delete the actual policy + options = { + 'http_method': 'delete', + 'resource_path': resource_path + '/' + actual_policy_moid + '/Profiles', + 'moid': moid, + } + intersight.call_api(**options) + actual_policy_moid = '' + if not actual_policy_moid: + if not intersight.module.check_mode: + # post profile to the expected policy + options = { + 'http_method': 'post', + 'resource_path': resource_path + '/' + expected_policy_moid + '/Profiles', + 'body': [ + { + 'ObjectType': 'server.Profile', + 'Moid': moid, + } + ] + } + intersight.call_api(**options) + intersight.result['changed'] = True + + +def main(): + argument_spec = intersight_argument_spec + argument_spec.update( + state=dict(type='str', choices=['present', 'absent'], default='present'), + organization=dict(type='str', default='default'), + name=dict(type='str', required=True), + target_platform=dict(type='str', choices=['Standalone', 'FIAttached'], default='Standalone'), + tags=dict(type='list', default=[]), + description=dict(type='str', aliases=['descr'], default=''), + assigned_server=dict(type='str', default=''), + boot_order_policy=dict(type='str'), + imc_access_policy=dict(type='str'), + lan_connectivity_policy=dict(type='str'), + local_user_policy=dict(type='str'), + ntp_policy=dict(type='str'), + storage_policy=dict(type='str'), + virtual_media_policy=dict(type='str'), + ) + + module = AnsibleModule( + argument_spec, + supports_check_mode=True, + ) + + intersight = IntersightModule(module) + intersight.result['api_response'] = {} + intersight.result['trace_id'] = '' + # + # Argument spec above, resource path, and API body should be the only code changed in this module + # + resource_path = '/server/Profiles' + # Define API body used in compares or create + intersight.api_body = { + 'Organization': { + 'Name': intersight.module.params['organization'], + }, + 'Name': intersight.module.params['name'], + 'Tags': intersight.module.params['tags'], + 'Description': intersight.module.params['description'], + } + intersight.result['api_response'] = {} + # Get assigned server information (if defined) + if intersight.module.params['assigned_server']: + intersight.get_resource( + resource_path='/compute/PhysicalSummaries', + query_params={ + '$filter': "Moid eq '" + intersight.module.params['assigned_server'] + "'", + } + ) + source_object_type = None + if intersight.result['api_response'].get('SourceObjectType'): + source_object_type = intersight.result['api_response']['SourceObjectType'] + intersight.api_body['AssignedServer'] = { + 'Moid': intersight.module.params['assigned_server'], + 'ObjectType': source_object_type, + } + if intersight.module.params['target_platform'] == 'FIAttached': + intersight.api_body['TargetPlatform'] = intersight.module.params['target_platform'] + + # Configure the profile + moid = intersight.configure_policy_or_profile(resource_path=resource_path) + + if moid and intersight.module.params['boot_order_policy']: + post_profile_to_policy(intersight, moid, resource_path='/boot/PrecisionPolicies', policy_name=intersight.module.params['boot_order_policy']) + + if moid and intersight.module.params['imc_access_policy']: + post_profile_to_policy(intersight, moid, resource_path='/access/Policies', policy_name=intersight.module.params['imc_access_policy']) + + if moid and intersight.module.params['lan_connectivity_policy']: + post_profile_to_policy(intersight, moid, resource_path='/vnic/LanConnectivityPolicies', policy_name=intersight.module.params['lan_connectivity_policy']) + + if moid and intersight.module.params['local_user_policy']: + post_profile_to_policy(intersight, moid, resource_path='/iam/EndPointUserPolicies', policy_name=intersight.module.params['local_user_policy']) + + if moid and intersight.module.params['ntp_policy']: + post_profile_to_policy(intersight, moid, resource_path='/ntp/Policies', policy_name=intersight.module.params['ntp_policy']) + + if moid and intersight.module.params['storage_policy']: + post_profile_to_policy(intersight, moid, resource_path='/storage/StoragePolicies', policy_name=intersight.module.params['storage_policy']) + + if moid and intersight.module.params['virtual_media_policy']: + post_profile_to_policy(intersight, moid, resource_path='/vmedia/Policies', policy_name=intersight.module.params['virtual_media_policy']) + + module.exit_json(**intersight.result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/cisco/intersight/plugins/modules/intersight_target_claim.py b/ansible_collections/cisco/intersight/plugins/modules/intersight_target_claim.py new file mode 100644 index 00000000..0a19e879 --- /dev/null +++ b/ansible_collections/cisco/intersight/plugins/modules/intersight_target_claim.py @@ -0,0 +1,171 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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 +from ansible_collections.cisco.intersight.plugins.module_utils.intersight import IntersightModule, intersight_argument_spec +from ansible.module_utils.basic import AnsibleModule +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: intersight_target_claim +short_description: Target claim configuraiton for Cisco Intersight +description: +- Target claim configuraiton for Cisco Intersight +- Used to claim or unclaim a Target from Cisco Intersight +- For more information see L(Cisco Intersight,https://intersight.com/apidocs). +extends_documentation_fragment: intersight +options: + claim_code: + description: + - Claim code required for registering a new Target + - Required if I(state=present) + type: str + required: no + device_id: + description: + - Device id (serial number) of target + - Targets containing multiple Target ids (e.g. IMM) can be formatted as <target1_id>&<target2_id> + type: dict + required: yes + state: + description: + - If C(present), will verify the resource is present and will create if needed. + - If C(absent), will verify the resource is absent and will delete if needed. + choices: [present, absent] + default: present +author: +- Brandon Beck (@techBeck03) +- CiscoUcs (@CiscoUcs) +version_added: '2.8' +''' + +EXAMPLES = r''' +- name: Claim new Target + cisco.intersight.intersight_target_claim: + device_id: "{{ device_id }}" + claim_code: "{{ claim_code }}" + state: present + +- name: Delete a Target (unclaim) + cisco.intersight.intersight_target_claim: + device_id: "{{ device_id }}" + state: absent +''' + +RETURN = r''' +api_repsonse: + description: The API response output returned by the specified resource. + returned: always + type: dict + sample: + "api_response": { + "Account": { + "ClassId": "mo.MoRef", + "Moid": "8675309", + "ObjectType": "iam.Account", + "link": "https://www.intersight.com/api/v1/iam/Accounts/8675309" + }, + "AccountMoid": "8675309", + "Ancestors": null, + "ClassId": "asset.DeviceClaim", + "CreateTime": "2021-05-10T17:32:13.522665238Z", + "Device": { + "ClassId": "mo.MoRef", + "Moid": "9035768", + "ObjectType": "asset.DeviceRegistration", + "link": "https://www.intersight.com/api/v1/asset/DeviceRegistrations/9035768" + }, + "DisplayNames": { + "short": [ + "FDO241604EM&FDO24161700" + ] + }, + "DomainGroupMoid": "5b4e48a96a636d6d346cd1c5", + "ModTime": "2021-05-10T17:32:13.522665238Z", + "Moid": "8675309", + "ObjectType": "asset.DeviceClaim", + "Owners": [ + "90357688675309" + ], + "PermissionResources": null, + "SecurityToken": "A95486674376E", + "SerialNumber": "FDO86753091&FDO86753092", + "SharedScope": "", + "Tags": [], + "trace_id": "NB3e883980a98adace8f7b9c2409cced1a" + } +''' + + +def main(): + argument_spec = intersight_argument_spec + argument_spec.update( + claim_code=dict(type='str'), + device_id=dict(type='str', required=True), + state=dict(type='str', choices=['absent', 'present'], default='present'), + ) + + module = AnsibleModule( + argument_spec, + supports_check_mode=True, + required_if=[ + ('state', 'present', (['claim_code']), False), + ] + ) + + intersight = IntersightModule(module) + intersight.result['api_response'] = {} + intersight.result['trace_id'] = '' + + # Check if device already exists in target list + target_ids = module.params['device_id'].split('&') + target_filter = '' + for idx, target_id in enumerate(target_ids): + if idx == 0: + target_filter += f"contains(TargetId,'{target_id}')" + else: + target_filter += f" or contains(TargetId,'{target_id}')" + intersight.get_resource( + resource_path='/asset/Targets', + query_params={ + "$select": "TargetId,RegisteredDevice", + "$filter": target_filter, + "$expand": "RegisteredDevice($select=DeviceClaim)" + }, + return_list=False, + ) + + if module.params['state'] == 'present': + # Send claim request if device id not already claimed + if not intersight.result['api_response']: + intersight.configure_resource( + moid=None, + resource_path='/asset/DeviceClaims', + body=dict( + SecurityToken=module.params['claim_code'], + SerialNumber=module.params['device_id'] + ), + query_params=None, + update_method='post' + ) + + elif module.params['state'] == 'absent': + # Check if target exists + if intersight.result['api_response'].get('Moid'): + intersight.delete_resource( + moid=intersight.result['api_response'].get('RegisteredDevice').get('DeviceClaim').get('Moid'), + resource_path='/asset/DeviceClaims', + ) + + module.exit_json(**intersight.result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/cisco/intersight/plugins/modules/intersight_virtual_media_policy.py b/ansible_collections/cisco/intersight/plugins/modules/intersight_virtual_media_policy.py new file mode 100644 index 00000000..22ab2ddd --- /dev/null +++ b/ansible_collections/cisco/intersight/plugins/modules/intersight_virtual_media_policy.py @@ -0,0 +1,368 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: intersight_virtual_media_policy +short_description: Virtual Media policy configuration for Cisco Intersight +description: + - Virtual Media policy configuration for Cisco Intersight. + - Used to configure Virtual Media image mappings on Cisco Intersight managed devices. + - For more information see L(Cisco Intersight,https://intersight.com/apidocs). +extends_documentation_fragment: intersight +options: + state: + description: + - If C(present), will verify the resource is present and will create if needed. + - If C(absent), will verify the resource is absent and will delete if needed. + choices: [present, absent] + default: present + organization: + description: + - The name of the Organization this resource is assigned to. + - Profiles and Policies that are created within a Custom Organization are applicable only to devices in the same Organization. + default: default + name: + description: + - The name assigned to the NTP policy. + - The name must be between 1 and 62 alphanumeric characters, allowing special characters :-_. + required: true + tags: + description: + - List of tags in Key:<user-defined key> Value:<user-defined value> format. + type: list + descrption: + description: + - The user-defined description of the NTP policy. + - Description can contain letters(a-z, A-Z), numbers(0-9), hyphen(-), period(.), colon(:), or an underscore(_). + aliases: [descr] + enable: + description: + - Enable or disable virtual media. + type: bool + default: true + encryption: + description: + - If enabled, allows encryption of all Virtual Media communications + type: bool + default: false + low_power_usb: + description: + - If enabled, the virtual drives appear on the boot selection menu after mapping the image and rebooting the host. + type: bool + default: true + cdd_virtual_media: + description: + - CDD Virtual Media image mapping options. + suboptions: + enable: + description: + - Enable or disable CDD image mapping. + type: bool + default: true + mount_type: + description: + - Type (protocol) of network share used by the remote_hostname. + - Ensure that the remote_hostname's communication port for the mount type that you choose is accessible from the managed endpoint. + - For CIFS as your mount type, ensure port 445 (which is its communication port) on the remote_hostname is accessible. + - For HTTP, ensure port 80 is accessible. + - For HTTPS, ensure port 443 is accessible. + - For NFS, ensure port 2049 is accessible. + choices: [nfs,cifs,http,https] + required: true + volume: + description: + - A user defined name of the image mounted for mapping. + required: true + remote_hostname: + description: + - Hostname or IP address of the server hosting the virtual media image. + required: true + remote_path: + description: + - Filepath (not including the filename) of the remote image. + - Ex. mnt/SHARE/ISOS + required: true + remote_file: + description: + - Filename of the remote image. + - Ex. custom_image.iso + required: true + username: + description: + - The username for the specified Mount Type, if required. + password: + description: + - The password for the selected username, if required. + hdd_virtual_media: + description: + - HDD Virtual Media image mapping options. + suboptions: + enable: + description: + - Enable or disable HDD image mapping. + type: bool + default: false + mount_type: + description: + - Type (protocol) of network share used by the remote_hostname. + - Ensure that the remote_hostname's communication port for the mount type that you choose is accessible from the managed endpoint. + - For CIFS as your mount type, ensure port 445 (which is its communication port) on the remote_hostname is accessible. + - For HTTP, ensure port 80 is accessible. + - For HTTPS, ensure port 443 is accessible. + - For NFS, ensure port 2049 is accessible. + choices: [nfs,cifs,http,https] + required: true + volume: + description: + - A user defined name of the image mounted for mapping. + required: true + remote_hostname: + description: + - Hostname or IP address of the server hosting the virtual media image. + required: true + remote_path: + description: + - Filepath (not including the filename) of the remote image. + - Ex. mnt/SHARE/ISOS + required: true + remote_file: + description: + - Filename of the remote image. + - Ex. custom_image.iso + required: true + username: + description: + - The username for the specified Mount Type, if required. + password: + description: + - The password for the selected username, if required. + mount_options: + description: + - Mount options for the Virtual Media mapping. + - For NFS, supported options are ro, rw, nolock, noexec, soft, port=VALUE, timeo=VALUE, retry=VALUE + - For CIFS, supported options are soft, nounix, noserverino, guest + required: false + authentication_protocol: + description: + - Authentication Protocol for CIFS Mount Type + required: false +author: + - David Soper (@dsoper2) + - Sid Nath (@SidNath21) +version_added: '2.10' +''' + +EXAMPLES = r''' +- name: Configure Virtual Media Policy + cisco.intersight.intersight_virtual_media_policy: + api_private_key: "{{ api_private_key }}" + api_key_id: "{{ api_key_id }}" + organization: DevNet + name: lab-vmedia + description: Virutal Media policy for lab use + tags: + - Key: Site + Value: RCDN + cdd_virtual_media: + mount_type: nfs + volume: nfs-cdd + remote_hostname: 172.28.224.77 + remote_path: mnt/SHARE/ISOS/CENTOS + remote_file: CentOS7.iso + hdd_virtual_media: + mount_type: nfs + volume: nfs-hdd + remote_hostname: 172.28.224.77 + remote_path: mnt/SHARE/ISOS/CENTOS + remote_file: CentOS7.iso + +- name: Delete Virtual Media Policy + cisco.intersight.intersight_virtual_media_policy: + api_private_key: "{{ api_private_key }}" + api_key_id: "{{ api_key_id }}" + organization: DevNet + name: lab-vmedia + state: absent +''' + +RETURN = r''' +api_repsonse: + description: The API response output returned by the specified resource. + returned: always + type: dict + sample: + "api_response": { + "Name": "lab-ntp", + "ObjectType": "ntp.Policy", + "Tags": [ + { + "Key": "Site", + "Value": "RCDN" + } + ] + } +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.intersight.plugins.module_utils.intersight import IntersightModule, intersight_argument_spec, compare_values + + +def main(): + path = '/vmedia/Policies' + virtual_media_mapping = dict( + enable=dict(type='bool', default=True), + mount_type=dict(type='str', choices=['nfs', 'cifs', 'http', 'https'], required=True), + volume=dict(type='str', required=True), + remote_hostname=dict(type='str', required=True), + remote_path=dict(type='str', required=True), + remote_file=dict(type='str', required=True), + mount_options=dict(type='str', default=''), + username=dict(type='str', default=''), + password=dict(type='str', default='', no_log=True), + authentication_protocol=dict(type='str', default='none'), + ) + argument_spec = intersight_argument_spec + argument_spec.update( + state=dict(type='str', choices=['present', 'absent'], default='present'), + organization=dict(type='str', default='default'), + name=dict(type='str', required=True), + description=dict(type='str', aliases=['descr'], default=''), + tags=dict(type='list', default=[]), + enable=dict(type='bool', default=True), + encryption=dict(type='bool', default=False), + low_power_usb=dict(type='bool', default=True), + cdd_virtual_media=dict(type='dict', options=virtual_media_mapping), + hdd_virtual_media=dict(type='dict', options=virtual_media_mapping), + ) + + module = AnsibleModule( + argument_spec, + supports_check_mode=True, + ) + + intersight = IntersightModule(module) + intersight.result['api_response'] = {} + intersight.result['trace_id'] = '' + # Defined API body used in compares or create + intersight.api_body = { + 'Organization': { + 'Name': intersight.module.params['organization'], + }, + 'Name': intersight.module.params['name'], + 'Tags': intersight.module.params['tags'], + 'Description': intersight.module.params['description'], + 'Enabled': intersight.module.params['enable'], + "Encryption": intersight.module.params['encryption'], + "LowPowerUsb": intersight.module.params['low_power_usb'], + 'Mappings': [], + } + + if intersight.module.params.get('cdd_virtual_media'): + intersight.api_body['Mappings'].append( + { + "ClassId": "vmedia.Mapping", + "ObjectType": "vmedia.Mapping", + "AuthenticationProtocol": intersight.module.params['cdd_virtual_media']['authentication_protocol'], + "DeviceType": "cdd", + "HostName": intersight.module.params['cdd_virtual_media']['remote_hostname'], + "Password": intersight.module.params['cdd_virtual_media']['password'], + "IsPasswordSet": intersight.module.params['cdd_virtual_media']['password'] != '', + "MountOptions": intersight.module.params['cdd_virtual_media']['mount_options'], + "MountProtocol": intersight.module.params['cdd_virtual_media']['mount_type'], + "RemoteFile": intersight.module.params['cdd_virtual_media']['remote_file'], + "RemotePath": intersight.module.params['cdd_virtual_media']['remote_path'], + "Username": intersight.module.params['cdd_virtual_media']['username'], + "VolumeName": intersight.module.params['cdd_virtual_media']['volume'], + } + ) + if intersight.module.params.get('hdd_virtual_media'): + intersight.api_body['Mappings'].append( + { + "ClassId": "vmedia.Mapping", + "ObjectType": "vmedia.Mapping", + "AuthenticationProtocol": intersight.module.params['hdd_virtual_media']['authentication_protocol'], + "DeviceType": "hdd", + "HostName": intersight.module.params['hdd_virtual_media']['remote_hostname'], + "Password": intersight.module.params['hdd_virtual_media']['password'], + "IsPasswordSet": intersight.module.params['hdd_virtual_media']['password'] != '', + "MountOptions": intersight.module.params['hdd_virtual_media']['mount_options'], + "MountProtocol": intersight.module.params['hdd_virtual_media']['mount_type'], + "RemoteFile": intersight.module.params['hdd_virtual_media']['remote_file'], + "RemotePath": intersight.module.params['hdd_virtual_media']['remote_path'], + "Username": intersight.module.params['hdd_virtual_media']['username'], + "VolumeName": intersight.module.params['hdd_virtual_media']['volume'], + } + ) + + organization_moid = None + # GET Organization Moid + intersight.get_resource( + resource_path='/organization/Organizations', + query_params={ + '$filter': "Name eq '" + intersight.module.params['organization'] + "'", + '$select': 'Moid', + }, + ) + if intersight.result['api_response'].get('Moid'): + # resource exists and moid was returned + organization_moid = intersight.result['api_response']['Moid'] + + intersight.result['api_response'] = {} + # get the current state of the resource + filter_str = "Name eq '" + intersight.module.params['name'] + "'" + filter_str += "and Organization.Moid eq '" + organization_moid + "'" + intersight.get_resource( + resource_path=path, + query_params={ + '$filter': filter_str, + '$expand': 'Organization', + }, + ) + + moid = None + resource_values_match = False + if intersight.result['api_response'].get('Moid'): + # resource exists and moid was returned + moid = intersight.result['api_response']['Moid'] + if module.params['state'] == 'present': + resource_values_match = compare_values(intersight.api_body, intersight.result['api_response']) + else: # state == 'absent' + intersight.delete_resource( + moid=moid, + resource_path=path, + ) + moid = None + + if module.params['state'] == 'present' and not resource_values_match: + # remove read-only Organization key + intersight.api_body.pop('Organization') + if not moid: + # Organization must be set, but can't be changed after initial POST + intersight.api_body['Organization'] = { + 'Moid': organization_moid, + } + intersight.configure_resource( + moid=moid, + resource_path=path, + body=intersight.api_body, + query_params={ + '$filter': filter_str, + }, + ) + + module.exit_json(**intersight.result) + + +if __name__ == '__main__': + main() |