diff --git a/lib/ansible/module_utils/vmware_httpapi/VmwareRestModule.py b/lib/ansible/module_utils/vmware_httpapi/VmwareRestModule.py new file mode 100644 index 00000000000..b4949e9bac6 --- /dev/null +++ b/lib/ansible/module_utils/vmware_httpapi/VmwareRestModule.py @@ -0,0 +1,708 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Paul Knight +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import re +import sys + +from ansible.module_utils.connection import Connection +from ansible.module_utils.basic import AnsibleModule, env_fallback + +# VMware ReST APIs +# +# Describes each supported VMware ReST APIs and lists its base URL. All +# vSphere ReST APIs begin with '/rest'. +API = dict( + appliance=dict(base='/rest/appliance'), + cis=dict(base='/rest/com/vmware/cis'), + content=dict(base='/rest/com/vmware/content'), + vapi=dict(base='/rest'), + vcenter=dict(base='/rest/vcenter'), + vrops=dict(base='/suiteapi') +) + +# Query Filters +# +# This dictionary identifies every valid filter that can be applied to a +# vSphere ReST API query. Each filter has a name, which may be the same +# depending on the type object; an id of the value specified; a type, +# which is typically either a string or a list. If it is a string, the +# format of the expected values is provided as a regex. +FILTER = dict( + clusters=dict( + name='clusters', + id='id', + type='str', + format=r'domain\-[0-9a-fA-F]+', + ), + connection_states=dict( + name='connection_states', + id='connection state', + type='list', + choices=[ + 'CONNECTED', + 'DISCONNECTED', + 'NOT_RESPONDING', + ], + ), + datacenters=dict( + name='datacenters', + id='id', + type='str', + format=r'datacenter\-[0-9a-fA-F]+', + ), + datastore_types=dict( + name='types', + id='type', + type='list', + choices=[ + '', + 'CIFS', + 'NFS', + 'NFS41', + 'VFFS', + 'VMFS', + 'VSAN', + 'VVOL', + ] + ), + datastores=dict( + name='datastores', + id='id', + type='str', + format=r'datastore\-[0-9a-fA-F]+', + ), + folder_types=dict( + name='type', + id='type', + type='list', + choices=[ + '', + 'DATACENTER', + 'DATASTORE', + 'HOST', + 'NETWORK', + 'VIRTUAL_MACHINE', + ] + ), + folders=dict( + name='folders', + id='id', + type='str', + format=r'group\-[hnv][0-9a-fA-F]+', + ), + hosts=dict( + name='hosts', + id='id', + type='str', + format=r'host\-[0-9a-fA-F]+', + ), + names=dict( + name='names', + id='name', + type='str', + format=r'.+', + ), + network_types=dict( + name='types', + id='type', + type='list', + choices=[ + 'DISTRIBUTED_PORTGROUP', + 'OPAQUE_NETWORK', + 'STANDARD_PORTGROUP', + ], + ), + networks=dict( + name='networks', + id='id', + type='str', + format=r'[dvportgroup|network]\-[0-9a-fA-F]+', + ), + parent_folders=dict( + name='parent_folders', + id='id', + type='str', + format=r'group\-[hnv][0-9a-fA-F]+', + ), + parent_resource_pools=dict( + name='parent_resource_pools', + id='id', + type='str', + format=r'resgroup\-[0-9a-fA-F]+', + ), + policies=dict( + name='policies', + id='GUID', + type='str', + format=(r'[0-9a-fA-F]{8}' + r'\-[0-9a-fA-F]{4}' + r'\-[0-9a-fA-F]{4}' + r'\-[0-9a-fA-F]{4}' + r'\-[0-9a-fA-F]{12}'), + ), + power_states=dict( + name='power_states', + id='power state', + type='list', + choices=[ + '', + 'POWERED_OFF', + 'POWERED_ON', + 'SUSPENDED', + ], + ), + resource_pools=dict( + name='resource_pools', + id='id', + type='str', + format=r'resgroup\-[0-9a-fA-F]+', + ), + status=dict( + name='status', + id='status', + type='list', + choices=[ + 'COMPLIANT', + 'NON_COMPLIANT', + 'NOT_APPLICABLE', + 'UNKNOWN', + 'UNKNOWN_COMPLIANCE', + 'OUT_OF_DATE', + ], + ), + vms=dict( + name='vms', + id='id', + type='str', + format=r'vm\-[0-9a-fA-F]+', + ), +) + +# vSphere Inventory Objects +# +# This dictionary lists the queryable vSphere inventory objects. Each +# object identifies the API it is managed through, its URL off of the +# API's base, and a list of filters that are valid for this particular +# object. +# +# NOTE: This will be replaced with a class factory pattern as get_id() +# and the get_url() family are tied to this structure. +INVENTORY = dict( + category=dict( + api='cis', + url='/tagging/category', + filters=[], + ), + cluster=dict( + api='vcenter', + url='/cluster', + filters=[ + 'clusters', + 'datacenters', + 'folders', + 'names', + ], + ), + content_library=dict( + api='content', + url='/library', + filters=[], + ), + content_type=dict( + api='content', + url='/type', + filters=[], + ), + datacenter=dict( + api='vcenter', + url='/datacenter', + filters=[ + 'datacenters', + 'folders', + 'names', + ], + ), + datastore=dict( + api='vcenter', + url='/datastore', + filters=[ + 'datacenters', + 'datastore_types', + 'datastores', + 'folders', + 'names', + ], + ), + folder=dict( + api='vcenter', + url='/folder', + filters=[ + 'datacenters', + 'folder_types', + 'folders', + 'names', + 'parent_folders', + ], + ), + host=dict( + api='vcenter', + url='/host', + filters=[ + 'clusters', + 'connection_states', + 'datacenters', + 'folders', + 'hosts', + 'names', + ], + ), + local_library=dict( + api='content', + url='/local-library', + filters=[], + ), + network=dict( + api='vcenter', + url='/network', + filters=[ + 'datacenters', + 'folders', + 'names', + 'network_types', + 'networks', + ], + ), + resource_pool=dict( + api='vcenter', + url='/resource-pool', + filters=[ + 'clusters', + 'datacenters', + 'hosts', + 'names', + 'parent_resource_pools', + 'resource_pools', + ] + ), + storage_policy=dict( + api='vcenter', + url='/storage/policies', + filters=[ + 'policies', + 'status', + 'vms', + ], + ), + subscribed_library=dict( + api='content', + url='/subscribed-library', + filters=[], + ), + tag=dict( + api='cis', + url='/tagging/tag', + filters=[], + ), + vm=dict( + api='vcenter', + url='/vm', + filters=[ + 'clusters', + 'datacenters', + 'folders', + 'hosts', + 'names', + 'power_states', + 'resource_pools', + 'vms', + ], + ), +) + + +class VmwareRestModule(AnsibleModule): + + def __init__(self, is_multipart=False, use_object_handler=False, *args, **kwargs): + '''Constructor - This module mediates interactions with the + VMware httpapi connector plugin, implementing VMware's ReST API. + + :module: VmwareRestModule extended from AnsibleModule. + :kw is_multipart: Indicates whether module makes multiple API calls. + Default False + :kw use_object_handler: Indicates whether module supports + multiple object types. Default False + ''' + # Initialize instance arguments + self.is_multipart = is_multipart + self.use_object_handler = use_object_handler + + # Output of module + self.result = {} + + # Current key of output + self.key = None + + # Current information going to httpapi + self.request = dict( + url=None, + filter=None, + data={}, + method=None, + ) + + # Last response from httpapi + self.response = dict( + status=None, + data={}, + ) + + # Initialize AnsibleModule superclass before params + super(VmwareRestModule, self).__init__(*args, **kwargs) + + # Params + # + # REQUIRED: Their absence will chuck a rod + self.allow_multiples = self.params['allow_multiples'] + self.status_code = self.params['status_code'] + # OPTIONAL: Use params.get() to gracefully fail + self.filters = self.params.get('filters') + self.state = self.params.get('state') + + # Initialize connection via httpapi connector. See "REST API Calls" + self._connection = Connection(self._socket_path) + + # Register default status handlers. See "Dynamic Status Handlers" + self._status_handlers = { + 'success': self.handle_default_success, + '401': self.handle_default_401, + '404': self.handle_default_404, + 'default': self.handle_default_generic, + } + if self.use_object_handler: + self._status_handlers['default'] = self.handle_default_object + + # Turn on debug if not specified, but ANSIBLE_DEBUG is set + self.module_debug = {} + if self._debug: + self.warn('Enable debug output because ANSIBLE_DEBUG was set.') + self.params['log_level'] = 'debug' + self.log_level = self.params['log_level'] + + # Debugging + # + # Tools to handle debugging output from the APIs. + def _mod_debug(self, key, **kwargs): + self.module_debug[key] = kwargs + if 'module_debug' not in self.module_debug: + self.module_debug = dict(key=kwargs) + else: + self.module_debug.update(key=kwargs) + + def _api_debug(self): + '''Route debugging output to the module output. + + NOTE: Adding self.path to result['path'] causes an absent in + output. Adding response['data'] causes infinite loop. + ''' + return dict( + url=self.request['url'], + filter=self.request['filter'], + data=self.request['data'], + method=self.request['method'], + status=self.response['status'], + state=self.state, + ) + + # Dynamic Status Handlers + # + # A handler is registered by adding its key, either a module- + # generated value, or the string representation of the status code; + # and the name of the handler function. The provided handlers are + # success defined, by default, as a status code of 200, but + # can be redefined, per module, using the status_code + # parameter in that module's argument_spec. + # 401 Unauthorized access to the API. + # 404 Requested object or API was not found. + # default Any status code not otherwise identified. + # The default handlers are named 'handle_default_[status_code]'. + # User defined handlers should use 'handle_[status_code]' as a + # convention. Note that if the module expects to handle more than + # one type of object, a default object handler replaces the default + # generic handler. + # + # Handlers do not take any arguments, instead using the instance's + # variables to determine the status code and any additional data, + # like object_type. To create or replace a handler, extend this + # class, define the new handler and use the provided 'set_handler' + # method. User handlers can also chain to the default handlers if + # desired. + def set_handler(self, status_key, handler): + '''Registers the handler to the status_key''' + self._status_handlers[status_key] = handler + + def _use_handler(self): + '''Invokes the appropriate handler based on status_code''' + if self.response['status'] in self.status_code: + status_key = 'success' + else: + status_key = str(self.response['status']) + if status_key in self._status_handlers.keys(): + self._status_handlers[status_key]() + else: + self._status_handlers['default']() + + def handle_default_success(self): + '''Default handler for all successful status codes''' + self.result[self.key] = self.response['data'] + if self.log_level == 'debug': + self.result[self.key].update( + debug=self._api_debug() + ) + if not self.is_multipart: + self.exit() + + def handle_default_401(self): + '''Default handler for Unauthorized (401) errors''' + self.fail(msg="Unable to authenticate. Provided credentials are not valid.") + + def handle_default_404(self): + '''Default handler for Not-Found (404) errors''' + self.fail(msg="Requested object was not found.") + + def handle_default_generic(self): + '''Catch-all handler for all other status codes''' + msg = self.response['data']['value']['messages'][0]['default_message'] + self.fail(msg=msg) + + def handle_default_object(self): + '''Catch-all handler capable of distinguishing multiple objects''' + try: + msg = self.response['data']['value']['messages'][0]['default_message'] + except (KeyError, TypeError): + msg = 'Unable to find the %s object specified due to %s' % (self.key, self.response) + self.fail(msg=msg) + + def handle_object_key_error(self): + '''Lazy exception handler''' + msg = ('Please specify correct object type to get information, ' + 'choices are [%s].' % ", ".join(list(INVENTORY.keys()))) + self.fail(msg=msg) + + # REST API Calls + # + # VMware's REST API uses GET, POST, PUT, PATCH and DELETE http + # calls to read, create, update and delete objects and their + # attributes. These calls are implemented as functions here. + def get(self, url='/rest', key='result'): + '''Sends a GET request to the httpapi plugin connection to the + specified URL. If successful, the returned data will be placed + in the output under the specified key. + ''' + self.request.update( + url=url, + data={}, + method='GET', + ) + self.key = key + self.response['status'], self.response['data'] = self._connection.send_request(url, {}, method='GET') + self._use_handler() + + def post(self, url='/rest', data=None, key='result'): + '''Sends a POST request to the httpapi plugin connection to the + specified URL, with the supplied data. If successful, any + returned data will be placed in the output under the specified + key. + ''' + self.request.update( + url=url, + data=data, + method='POST', + ) + self.key = key + self.response['status'], self.response['data'] = self._connection.send_request(url, data, method='POST') + self._use_handler() + + def put(self, url='/rest', data=None, key='result'): + '''Sends a PUT request to the httpapi plugin connection to the + specified URL, with the supplied data. If successful, any + returned data will be placed in the output under the specified + key. + ''' + self.request.update( + url=url, + data=data, + method='PUT', + ) + self.key = key + self.response['status'], self.response['data'] = self._connection.send_request(url, data, method='PUT') + self._use_handler() + + def delete(self, url='/rest', data='result', key='result'): + '''Sends a DELETE request to the httpapi plugin connection to + the specified URL, with the supplied data. If successful, any + returned data will be placed in the output under the specified + key. + ''' + self.request.update( + url=url, + data=data, + method='DELETE', + ) + self.key = key + self.response['status'], self.response['data'] = self._connection.send_request(url, data, method='DELETE') + self._use_handler() + + def get_id(self, object_type, name): + '''Find id(s) of object(s) with given name. allow_multiples + determines whether multiple IDs are returned or not. + + :kw object_type: The inventory object type whose id is desired. + :kw name: The name of the object(s) to be retrieved. + :returns: a list of strings representing the IDs of the objects. + ''' + + try: + url = (API[INVENTORY[object_type]['api']]['base'] + + INVENTORY[object_type]['url']) + if '/' in name: + name.replace('/', '%2F') + url += '&filter.names=' + name + except KeyError: + self.fail(msg='object_type must be one of [%s].' + % ", ".join(list(INVENTORY.keys()))) + + status, data = self._connection.send_request(url, {}, method='GET') + if status != 200: + self.request.update(url=url, data={}, method='GET') + self.response.update(status=status, data=data) + self.handle_default_generic() + + num_items = len(data['value']) + if not self.allow_multiples and num_items > 1: + msg = ('Found %d objects of type %s with name %s. ' + 'Set allow_multiples to True if this is expected.' + % (num_items, object_type, name)) + self.fail(msg=msg) + + ids = [] + for i in range(num_items): + ids += data[i][object_type] + return ids + + def _build_filter(self, object_type): + '''Builds a filter from the optionally supplied params''' + if self.filters: + try: + first = True + for filter in self.filters: + for key in list(filter.keys()): + filter_key = key.lower() + # Check if filter is valid for current object type or not + if filter_key not in INVENTORY[object_type]['filters']: + msg = ('%s is not a valid %s filter, choices are [%s].' + % (key, object_type, ", ".join(INVENTORY[object_type]['filters']))) + self.fail(msg=msg) + # Check if value is valid for the current filter + if ((FILTER[filter_key]['type'] == 'str' and not re.match(FILTER[filter_key]['format'], filter[key])) or + (FILTER[filter_key]['type'] == 'list' and filter[key] not in FILTER[filter_key]['choices'])): + msg = ('%s is not a valid %s %s' % (filter[key], object_type, FILTER[filter_key]['name'])) + self.fail(msg=msg) + if first: + self.request['filter'] = '?' + first = False + else: + self.request['filter'] += '&' + # Escape characters + if '/' in filter[key]: + filter[key].replace('/', '%2F') + self.request['filter'] += ('filter.%s=%s' + % (FILTER[filter_key]['name'], filter[key])) + except KeyError: + self.handle_object_key_error() + else: + self.request['filter'] = None + return self.request['filter'] + + def get_url(self, object_type, with_filter=False): + '''Retrieves the URL of a particular inventory object with or without filter''' + try: + self.url = (API[INVENTORY[object_type]['api']]['base'] + + INVENTORY[object_type]['url']) + if with_filter: + self.url += self._build_filter(object_type) + except KeyError: + self.handle_object_key_error + return self.url + + def get_url_with_filter(self, object_type): + '''Same as get_url, only with_filter is explicitly set''' + return self.get_url(object_type, with_filter=True) + + def reset(self): + '''Clears the decks for next request''' + self.request.update( + url=None, + data={}, + method=None, + ) + self.response.update( + status=None, + data={}, + ) + + def fail(self, msg): + if self.log_level == 'debug': + if self.request['url'] is not None: + self.result['debug'] = self._api_debug() + AnsibleModule.fail_json(self, msg=msg, **self.result) + + def exit(self): + '''Called to end client interaction''' + if 'invocation' not in self.result: + self.result['invocation'] = { + 'module_args': self.params, + 'module_kwargs': { + 'is_multipart': self.is_multipart, + 'use_object_handler': self.use_object_handler, + } + } + if self.log_level == 'debug': + if not self.is_multipart: + self.result['invocation'].update(debug=self._api_debug()) + if self.module_debug: + self.result['invocation'].update(module_debug=self.module_debug) + + AnsibleModule.exit_json(self, **self.result) + + def _merge_dictionaries(self, a, b): + new = a.copy() + new.update(b) + return new + + @staticmethod + def create_argument_spec(use_filters=False, use_state=False): + '''Provide a default argument spec for this module. Filters and + state are optional parameters dependinf on the module's needs. + Additional parameters can be added. The supplied parameters can + have defaults changed or choices pared down, but should not be + removed. + ''' + + argument_spec = dict( + allow_multiples=dict(type='bool', default=False), + log_level=dict(type='str', + choices=['debug', 'info', 'normal'], + default='normal'), + status_code=dict(type='list', default=[200]), + ) + if use_filters: + argument_spec.update(filters=dict(type='list', default=[])) + if use_state: + argument_spec.update(state=dict(type='list', + choices=['absent', 'present', 'query'], + default='query')) + return argument_spec diff --git a/lib/ansible/module_utils/vmware_httpapi/__init__.py b/lib/ansible/module_utils/vmware_httpapi/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/cloud/vmware_httpapi/__init__.py b/lib/ansible/modules/cloud/vmware_httpapi/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/cloud/vmware_httpapi/vmware_appliance_access_info.py b/lib/ansible/modules/cloud/vmware_httpapi/vmware_appliance_access_info.py new file mode 100644 index 00000000000..abdd47abd5c --- /dev/null +++ b/lib/ansible/modules/cloud/vmware_httpapi/vmware_appliance_access_info.py @@ -0,0 +1,117 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Paul Knight +# 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: vmware_appliance_access_info +short_description: Gathers info about modes of access to the vCenter appliance using REST API. +description: +- This module can be used to gather information about the four modes of accessing the VCSA. +- This module is based on REST API and uses httpapi connection plugin for persistent connection. +- The Appliance API works against the VCSA and uses the "administrator@vsphere.local" user. +version_added: '2.9' +author: +- Paul Knight (@n3pjk) +notes: +- Tested on vSphere 6.7 +requirements: +- python >= 2.6 +options: + access_mode: + description: + - Method of access to get to appliance + - If not specified, all modes will be returned. + required: false + choices: ['consolecli', 'dcui', 'shell', 'ssh'] + type: str +extends_documentation_fragment: VmwareRestModule.documentation +''' + +EXAMPLES = r''' +- hosts: all + connection: httpapi + gather_facts: false + vars: + ansible_network_os: vmware + ansible_host: vcenter.my.domain + ansible_user: administrator@vsphere.local + ansible_httpapi_password: "SomePassword" + ansbile_httpapi_use_ssl: yes + ansible_httpapi_validate_certs: false + tasks: + + - name: Get all access modes information + vmware_appliance_access_info: + + - name: Get ssh access mode information + vmware_appliance_access_info: + access_mode: ssh +''' + +RETURN = r''' +access_mode: + description: facts about the specified access mode + returned: always + type: dict + sample: { + "value": true + } +''' + +from ansible.module_utils.vmware_httpapi.VmwareRestModule import API, VmwareRestModule + + +SLUG = dict( + consolecli='/access/consolecli', + dcui='/access/dcui', + shell='/access/shell', + ssh='/access/ssh', +) + + +def get_mode(module, mode): + try: + url = API['appliance']['base'] + SLUG[mode] + except KeyError: + module.fail(msg='[%s] is not a valid access mode. ' + 'Please specify correct mode, valid choices are ' + '[%s].' % (mode, ", ".join(list(SLUG.keys())))) + + module.get(url=url, key=mode) + + +def main(): + argument_spec = VmwareRestModule.create_argument_spec() + argument_spec.update( + access_mode=dict(type='str', choices=['consolecli', 'dcui', 'shell', 'ssh'], default=None), + ) + + module = VmwareRestModule(argument_spec=argument_spec, + supports_check_mode=True, + is_multipart=True, + use_object_handler=True) + access_mode = module.params['access_mode'] + + if access_mode is None: + access_mode = SLUG.keys() + for mode in access_mode: + get_mode(module, mode) + else: + get_mode(module, access_mode) + + module.exit() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/vmware_httpapi/vmware_appliance_health_info.py b/lib/ansible/modules/cloud/vmware_httpapi/vmware_appliance_health_info.py new file mode 100644 index 00000000000..9acaaec721c --- /dev/null +++ b/lib/ansible/modules/cloud/vmware_httpapi/vmware_appliance_health_info.py @@ -0,0 +1,148 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Paul Knight +# 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: vmware_appliance_health_info +short_description: Gathers info about health of the VCSA. +description: +- This module can be used to gather information about VCSA health. +- This module is based on REST API and uses httpapi connection plugin for persistent connection. +- The Appliance API works against the VCSA and uses the "administrator@vsphere.local" user. +version_added: '2.9' +author: +- Paul Knight (@n3pjk) +notes: +- Tested on vSphere 6.7 +requirements: +- python >= 2.6 +options: + subsystem: + description: + - A subsystem of the VCSA. + required: false + choices: ['applmgmt', 'databasestorage', 'lastcheck', 'load', 'mem', 'softwarepackages', 'storage', 'swap', 'system'] + type: str + asset: + description: + - A VCSA asset that has associated health metrics. + - Valid choices have yet to be determined at this time. + required: false + type: str +extends_documentation_fragment: VmwareRestModule.documentation +''' + +EXAMPLES = r''' +- hosts: all + connection: httpapi + gather_facts: false + vars: + ansible_network_os: vmware + ansible_host: vcenter.my.domain + ansible_user: administrator@vsphere.local + ansible_httpapi_password: "SomePassword" + ansbile_httpapi_use_ssl: yes + ansible_httpapi_validate_certs: false + tasks: + + - name: Get all health attribute information + vmware_appliance_health_info: + + - name: Get system health information + vmware_appliance_health_info: + subsystem: system +''' + +RETURN = r''' +attribute: + description: facts about the specified health attribute + returned: always + type: dict + sample: { + "value": true + } +''' + +from ansible.module_utils.vmware_httpapi.VmwareRestModule import API, VmwareRestModule + + +SLUG = dict( + applmgmt='/health/applmgmt', + databasestorage='/health/database-storage', + load='/health/load', + mem='/health/mem', + softwarepackages='/health/software-packages', + storage='/health/storage', + swap='/health/swap', + system='/health/system', + lastcheck='/health/system/lastcheck', +) + + +def get_subsystem(module, subsystem): + try: + url = API['appliance']['base'] + SLUG[subsystem] + except KeyError: + module.fail(msg='[%s] is not a valid subsystem. ' + 'Please specify correct subsystem, valid choices are ' + '[%s].' % (subsystem, ", ".join(list(SLUG.keys())))) + + module.get(url=url, key=subsystem) + + +def main(): + argument_spec = VmwareRestModule.create_argument_spec() + argument_spec.update( + subsystem=dict( + type='str', + required=False, + choices=[ + 'applmgmt', + 'databasestorage', + 'lastcheck', + 'load', + 'mem', + 'softwarepackages', + 'storage', + 'swap', + 'system', + ], + ), + asset=dict(type='str', required=False), + ) + + module = VmwareRestModule(argument_spec=argument_spec, + supports_check_mode=True, + is_multipart=True, + use_object_handler=True) + subsystem = module.params['subsystem'] + asset = module.params['asset'] + + if asset is not None: + url = (API['appliance']['base'] + + ('/health/%s/messages' % asset)) + + module.get(url=url, key=asset) + elif subsystem is None: + subsystem = SLUG.keys() + for sys in subsystem: + get_subsystem(module, sys) + else: + get_subsystem(module, subsystem) + + module.exit() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/vmware_httpapi/vmware_cis_category_info.py b/lib/ansible/modules/cloud/vmware_httpapi/vmware_cis_category_info.py new file mode 100644 index 00000000000..5c294d510fb --- /dev/null +++ b/lib/ansible/modules/cloud/vmware_httpapi/vmware_cis_category_info.py @@ -0,0 +1,149 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Paul Knight +# 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: vmware_cis_category_info +short_description: Gathers info about all, or a specified category. +description: +- This module can be used to gather information about a specific category. +- This module can also gather facts about all categories. +- This module is based on REST API and uses httpapi connection plugin for persistent connection. +version_added: '2.9' +author: +- Paul Knight (@n3pjk) +notes: +- Tested on vSphere 6.7 +requirements: +- python >= 2.6 +options: + category_id: + description: + - The object id of the category. + - Exclusive of category_name and used_by_*. + required: false + type: str + category_name: + description: + - The name of the category. + - Exclusive of category_id and used_by_*. + required: false + type: str + used_by_id: + description: + - The id of the entity to list applied categories. + - Exclusive of other used_by_* and category_*. + type: str + used_by_name: + description: + - The name of the entity to list applied categories, whose type is specified in used_by_type. + - Exclusive of other used_by_id and category_*. + type: str + used_by_type: + description: + - The type of the entity to list applied categories, whose name is specified in used_by_name. + - Exclusive of other used_by_id and category_*. + choices: ['cluster', 'content_library', 'content_type', 'datacenter', + 'datastore', 'folder', 'host', 'local_library', 'network', + 'resource_pool', 'subscribed_library', 'tag', 'vm'] + type: str +extends_documentation_fragment: VmwareRestModule.documentation +''' + +EXAMPLES = r''' +- name: Get all categories + vmware_cis_category_info: +''' + +RETURN = r''' +category: + description: facts about the specified category + returned: always + type: dict + sample: { + "value": true + } +''' + +from ansible.module_utils.vmware_httpapi.VmwareRestModule import VmwareRestModule + + +def main(): + argument_spec = VmwareRestModule.create_argument_spec() + argument_spec.update( + category_name=dict(type='str', required=False), + category_id=dict(type='str', required=False), + used_by_name=dict(type='str', required=False), + used_by_type=dict( + type='str', + required=False, + choices=[ + 'cluster', + 'content_library', + 'content_type', + 'datacenter', + 'datastore', + 'folder', + 'host', + 'local_library', + 'network', + 'resource_pool', + 'subscribed_library', + 'tag', + 'vm', + ], + ), + used_by_id=dict(type='str', required=False), + ) + + required_together = [ + ['used_by_name', 'used_by_type'] + ] + + mutually_exclusive = [ + ['category_name', 'category_id', 'used_by_id', 'used_by_name'], + ['category_name', 'category_id', 'used_by_id', 'used_by_type'], + ] + + module = VmwareRestModule(argument_spec=argument_spec, + required_together=required_together, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + category_name = module.params['category_name'] + category_id = module.params['category_id'] + used_by_name = module.params['used_by_name'] + used_by_type = module.params['used_by_type'] + used_by_id = module.params['used_by_id'] + + url = module.get_url('category') + data = {} + if category_name is not None: + category_id = module.get_id('category', category_name) + if category_id is not None: + url += '/id:' + category_id + module.get(url=url) + else: + if used_by_name is not None: + used_by_id = module.get_id(used_by_type, used_by_name) + url += '?~action=list-used-categories' + data = { + 'used_by_entity': used_by_id + } + module.post(url=url, data=data) + module.exit() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/vmware_httpapi/vmware_core_info.py b/lib/ansible/modules/cloud/vmware_httpapi/vmware_core_info.py new file mode 100644 index 00000000000..74a4425c9fd --- /dev/null +++ b/lib/ansible/modules/cloud/vmware_httpapi/vmware_core_info.py @@ -0,0 +1,117 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Abhijeet Kasurde +# Copyright: (c) 2019, Paul Knight +# 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: vmware_core_info +short_description: Gathers info about various VMware inventory objects using REST API +description: +- This module can be used to gather information about various VMware inventory objects. +- This module is based on REST API and uses httpapi connection plugin for persistent connection. +version_added: '2.9' +author: +- Abhijeet Kasurde (@Akasurde) +- Paul Knight (@n3pjk) +notes: +- Tested on vSphere 6.7 +requirements: +- python >= 2.6 +options: + object_type: + description: + - Type of VMware object. + - Valid choices are datacenter, cluster, datastore, folder, host, + network, resource_pool, virtual_machine, content_library, + local_library, subscribed_library, content_type, tag, category. + type: str + default: datacenter + filters: + description: + - A list of filters to find the given object. + - Valid filters for datacenter object type - folders, datacenters, names. + - Valid filters for cluster object type - folders, datacenters, names, clusters. + - Valid filters for datastore object type - folders, datacenters, names, datastores, types. + - Valid filters for folder object type - folders, parent_folders, names, datacenters, type. + - Valid filters for host object type - folders, hosts, names, datacenters, clusters, connection_states. + - Valid filters for network object type - folders, types, names, datacenters, networks. + - Valid filters for resource_pool object type - resource_pools, parent_resource_pools, names, datacenters, hosts, clusters. + - Valid filters for virtual_machine object type - folders, resource_pools, power_states, vms, names, datacenters, hosts, clusters. + - content_library, local_library, subscribed_library, content_type, tag, category does not take any filters. + default: [] + type: list +extends_documentation_fragment: VmwareRestModule_filters.documentation +''' + +EXAMPLES = r''' +- name: Get All VM without any filters + block: + - name: Get VMs + vmware_core_info: + object_type: "{{ object_type }}" + register: vm_result + + - assert: + that: + - vm_result[object_type].value | length > 0 + vars: + object_type: vm + +- name: Get all clusters from Asia-Datacenter1 + vmware_core_info: + object_type: cluster + filters: + - datacenters: "{{ datacenter_obj }}" + register: clusters_result +''' + +RETURN = r''' +object_info: + description: information about the given VMware object + returned: always + type: dict + sample: { + "value": [ + { + "cluster": "domain-c42", + "drs_enabled": false, + "ha_enabled": false, + "name": "Asia-Cluster1" + } + ] + } +''' + +from ansible.module_utils.vmware_httpapi.VmwareRestModule import VmwareRestModule + + +def main(): + argument_spec = VmwareRestModule.create_argument_spec(use_filters=True) + argument_spec.update( + object_type=dict(type='str', default='datacenter'), + ) + + module = VmwareRestModule(argument_spec=argument_spec, + supports_check_mode=True, + use_object_handler=True) + object_type = module.params['object_type'] + + url = module.get_url_with_filter(object_type) + + module.get(url=url, key=object_type) + module.exit() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/doc_fragments/VmwareRestModule.py b/lib/ansible/plugins/doc_fragments/VmwareRestModule.py new file mode 100644 index 00000000000..8dbd6038ca4 --- /dev/null +++ b/lib/ansible/plugins/doc_fragments/VmwareRestModule.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Paul Knight +# 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 + + +class ModuleDocFragment(object): + # Parameters for VMware ReST HTTPAPI modules omits filters and state + DOCUMENTATION = r''' +options: + allow_multiples: + description: + - Indicates whether get_id() can return multiple IDs for a given name. + - Typically, this should be false when updating or deleting; otherwise, all named objects could be affected. + required: true + version_added: "2.9" + type: bool + log_level: + description: + - If ANSIBLE_DEBUG is set, this will be forced to 'debug', but can be user-defined otherwise. + required: True + choices: ['debug', 'info', 'normal'] + version_added: "2.9" + type: str + default: 'normal' + status_code: + description: + - A list of integer status codes considered to be successful for the this module. + required: true + version_added: "2.9" + type: list + default: [200] +''' diff --git a/lib/ansible/plugins/doc_fragments/VmwareRestModule_filters.py b/lib/ansible/plugins/doc_fragments/VmwareRestModule_filters.py new file mode 100644 index 00000000000..73c52de4d98 --- /dev/null +++ b/lib/ansible/plugins/doc_fragments/VmwareRestModule_filters.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Paul Knight +# 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 + + +class ModuleDocFragment(object): + # Parameters for VMware ReST HTTPAPI modules includes filters + DOCUMENTATION = r''' +options: + allow_multiples: + description: + - Indicates whether get_id() can return multiple IDs for a given name. + - Typically, this should be false when updating or deleting; otherwise, all named objects could be affected. + required: true + version_added: "2.9" + type: bool + filters: + description: + - The key/value pairs describing filters to be applied to the request(s) made by this instance. + required: false + version_added: "2.9" + type: dict + log_level: + description: + - If ANSIBLE_DEBUG is set, this will be forced to 'debug', but can be user-defined otherwise. + required: True + choices: ['debug', 'info', 'normal'] + version_added: "2.9" + type: str + default: 'normal' + status_code: + description: + - A list of integer status codes considered to be successful for the this module. + required: true + version_added: "2.9" + type: list + default: [200] +''' diff --git a/lib/ansible/plugins/doc_fragments/VmwareRestModule_full.py b/lib/ansible/plugins/doc_fragments/VmwareRestModule_full.py new file mode 100644 index 00000000000..3adf3b2b383 --- /dev/null +++ b/lib/ansible/plugins/doc_fragments/VmwareRestModule_full.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Paul Knight +# 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 + + +class ModuleDocFragment(object): + # Parameters for VMware ReST HTTPAPI modules includes filters and state + DOCUMENTATION = r''' +options: + allow_multiples: + description: + - Indicates whether get_id() can return multiple IDs for a given name. + - Typically, this should be false when updating or deleting; otherwise, all named objects could be affected. + required: true + version_added: "2.9" + type: bool + filters: + description: + - The key/value pairs describing filters to be applied to the request(s) made by this instance. + required: false + version_added: "2.9" + type: dict + log_level: + description: + - If ANSIBLE_DEBUG is set, this will be forced to 'debug', but can be user-defined otherwise. + required: True + choices: ['debug', 'info', 'normal'] + version_added: "2.9" + type: str + default: 'normal' + state: + description: + - Either 'absent' or 'present', depending on whether object should be removed or created. + required: false + choices: ['absent', 'present', 'query'] + version_added: "2.9" + type: str + default: 'present' + status_code: + description: + - A list of integer status codes considered to be successful for the this module. + required: true + version_added: "2.9" + type: list + default: [200] +''' diff --git a/lib/ansible/plugins/doc_fragments/VmwareRestModule_state.py b/lib/ansible/plugins/doc_fragments/VmwareRestModule_state.py new file mode 100644 index 00000000000..01b692ca9fe --- /dev/null +++ b/lib/ansible/plugins/doc_fragments/VmwareRestModule_state.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Paul Knight +# 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 + + +class ModuleDocFragment(object): + # Parameters for VMware ReST HTTPAPI modules includes filters and state + DOCUMENTATION = r''' +options: + allow_multiples: + description: + - Indicates whether get_id() can return multiple IDs for a given name. + - Typically, this should be false when updating or deleting; otherwise, all named objects could be affected. + required: true + version_added: "2.9" + type: bool + log_level: + description: + - If ANSIBLE_DEBUG is set, this will be forced to 'debug', but can be user-defined otherwise. + required: True + choices: ['debug', 'info', 'normal'] + version_added: "2.9" + type: str + default: 'normal' + state: + description: + - Either 'absent' or 'present', depending on whether object should be removed or created. + required: false + choices: ['absent', 'present', 'query'] + version_added: "2.9" + type: str + default: 'present' + status_code: + description: + - A list of integer status codes considered to be successful for the this module. + required: true + version_added: "2.9" + type: list + default: [200] +''' diff --git a/lib/ansible/plugins/httpapi/vmware.py b/lib/ansible/plugins/httpapi/vmware.py new file mode 100644 index 00000000000..1e20d667a18 --- /dev/null +++ b/lib/ansible/plugins/httpapi/vmware.py @@ -0,0 +1,85 @@ +# Copyright: (c) 2018 Red Hat Inc. +# Copyright: (c) 2019, Ansible Project +# Copyright: (c) 2019, Abhijeet Kasurde +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ +--- +author: Abhijeet Kasurde (Akasurde) +httpapi : vmware +short_description: HttpApi Plugin for VMware REST API +description: + - This HttpApi plugin provides methods to connect to VMware vCenter over a HTTP(S)-based APIs. +version_added: "2.9" +""" + +import json + +from ansible.module_utils.basic import to_text +from ansible.errors import AnsibleConnectionFailure +from ansible.module_utils.six.moves.urllib.error import HTTPError +from ansible.plugins.httpapi import HttpApiBase +from ansible.module_utils.connection import ConnectionError + +BASE_HEADERS = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', +} + + +class HttpApi(HttpApiBase): + def login(self, username, password): + if username and password: + payload = {} + url = '/rest/com/vmware/cis/session' + response, response_data = self.send_request(url, payload) + else: + raise AnsibleConnectionFailure('Username and password are required for login') + + if response == 404: + raise ConnectionError(response_data) + + if not response_data.get('value'): + raise ConnectionError('Server returned response without token info during connection authentication: %s' % response) + + self.connection._session_uid = "vmware-api-session-id:%s" % response_data['value'] + self.connection._token = response_data['value'] + + def logout(self): + response, dummy = self.send_request('/rest/com/vmware/cis/session', None, method='DELETE') + + def get_session_uid(self): + return self.connection._session_uid + + def get_session_token(self): + return self.connection._token + + def send_request(self, path, body_params, method='POST'): + data = json.dumps(body_params) if body_params else '{}' + + try: + self._display_request(method=method) + response, response_data = self.connection.send(path, data, method=method, headers=BASE_HEADERS, force_basic_auth=True) + response_value = self._get_response_value(response_data) + + return response.getcode(), self._response_to_json(response_value) + except AnsibleConnectionFailure as e: + return 404, 'Object not found' + except HTTPError as e: + return e.code, json.loads(e.read()) + + def _display_request(self, method='POST'): + self.connection.queue_message('vvvv', 'Web Services: %s %s' % (method, self.connection._url)) + + def _get_response_value(self, response_data): + return to_text(response_data.getvalue()) + + def _response_to_json(self, response_text): + try: + return json.loads(response_text) if response_text else {} + # JSONDecodeError only available on Python 3.5+ + except ValueError: + raise ConnectionError('Invalid JSON response: %s' % response_text)