diff --git a/plugins/module_utils/service_catalog.py b/plugins/module_utils/service_catalog.py new file mode 100644 index 00000000..6bbc5668 --- /dev/null +++ b/plugins/module_utils/service_catalog.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2024, Red Hat +# +# 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 + + +SN_BASE_PATH = "api/sn_sc/servicecatalog" + + +class ItemContent(object): + FULL = 1 + BRIEF = 2 + NONE = 3 + + @classmethod + def from_str(self, s): + if s == "full": + return ItemContent.FULL + if s == "brief": + return ItemContent.BRIEF + return ItemContent.NONE + + +class ServiceCatalogObject(object): + def to_ansible(self): + """Filters out the fields which we don't want to return like `header_icon`""" + ansible_data = dict() + for key in self.DISPLAY_FIELDS: + if key in self.data: + if isinstance(self.data[key], ServiceCatalogObject): + ansible_data[key] = self.data[key].to_ansible() + continue + if type(self.data[key]) is list: + ansible_data[key] = [] + for item in self.data[key]: + if isinstance(item, ServiceCatalogObject): + ansible_data[key].append(item.to_ansible()) + else: + ansible_data[key].append(item) + continue + ansible_data[key] = self.data[key] + return ansible_data + + def valid(self): + for key in self.MANDATORY_FIELS: + if key not in self.data or not self.data[key]: + return False + return True + + @property + def sys_id(self): + return self.data["sys_id"] if "sys_id" in self.data else "" + + +class Catalog(ServiceCatalogObject): + DISPLAY_FIELDS = ["sys_id", "description", "title", + "has_categories", "has_items", "categories", "items"] + MANDATORY_FIELS = ["sys_id"] + + def __init__(self, data=dict()): + self.data = data + self._categories = [] + self._items = [] + + @property + def categories(self): + return self._categories + + @categories.setter + def categories(self, categories): + self._categories = categories + + @property + def items(self): + return self._items + + @items.setter + def items(self, items): + self._items = items + + def to_ansible(self): + self.data["categories"] = self.categories + self.data["items"] = self.items + return super().to_ansible() + + +class Category(ServiceCatalogObject): + DISPLAY_FIELDS = ["sys_id", "description", "title", + "full_description", "subcategories"] + + def __init__(self, data=dict()): + self.data = data + + +class Item(ServiceCatalogObject): + DISPLAY_FIELDS = ["sys_id", "short_description", "description", + "availabiltiy", "mandatory_attachment", "request_method", + "type", "sys_class_name", "catalogs", "name", "category", "order", + "categories", "variables"] + + def __init__(self, data=dict()): + self.data = data + + +class ServiceCatalogClient(object): + """Wraps the generic client with Service Catalog specific methods""" + + def __init__(self, generic_client): + if not generic_client: + raise ValueError("generic client cannot be none") + self.generic_client = generic_client + + def get_catalogs(self): + """Returns the list of all catalogs""" + records = self.generic_client.list_records("/".join([SN_BASE_PATH, "catalogs"])) + if records: + return [Catalog(record) for record in records] + return [] + + def get_catalog(self, id): + """Returns the catalog identified by id""" + if not id: + raise ValueError("catalog sys_id is missing") + record = self.generic_client.get_record_by_sys_id("/".join([SN_BASE_PATH, "catalogs"]), id) + if record: + return Catalog(record) + return None + + def get_categories(self, catalog_id): + """Returns the list of all categories of the catalog `catalog_id`""" + if not id: + raise ValueError("catalog sys_id is missing") + records = self.generic_client.list_records( + "/".join([SN_BASE_PATH, "catalogs", catalog_id, "categories"])) + if records: + return [Category(record) for record in records] + return dict() + + def get_items(self, catalog_id, query=None, batch_size=1000): + """Returns the list of all items of the catalog `catalog_id`""" + if not id: + raise ValueError("catalog sys_id is missing") + _query = dict(sysparm_catalog=catalog_id) + if query: + _query.update(query) + self.generic_client.batch_size = batch_size + records = self.generic_client.list_records( + "/".join([SN_BASE_PATH, "items"]), _query) + if records: + return [Item(record) for record in records] + return dict() + + def get_item(self, id): + if not id: + raise ValueError("item sys_id is missing") + return Item(self.generic_client.get_record_by_sys_id("/".join([SN_BASE_PATH, "items"]), id)) + + diff --git a/plugins/modules/service_catalog_info.py b/plugins/modules/service_catalog_info.py new file mode 100644 index 00000000..454e3d8d --- /dev/null +++ b/plugins/modules/service_catalog_info.py @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2024, Red Hat +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = r""" +module: configuration_item_relations + +author: + - Cosmin Tupangiu (@tupyy) + +short_description: List ServiceNow service catalogs along with categories and items + +description: + - Retrive information about ServiceCatalogs + - For more information, refer to ServiceNow service catalog documentation at + U(https://developer.servicenow.com/dev.do#!/reference/api/tokyo/rest/c_ServiceCatalogAPI) + +version_added: 2.6.0 + +extends_documentation_fragment: + - servicenow.itsm.instance + - servicenow.itsm.sys_id.info + +options: + categories: + description: + - If true the cateogies will be fetched from SN + type: bool + items: + description: + - List of options for fetching service catalog items. + - If set the items for each catalog will be fetched. + type: dict + suboptions: + content: + description: + - Content type of the item + - Set to full if the whole item will be fetched + type: str + choices: [full, brief] + required: true + query: + description: + - Query for item content + - For more information, please refer to + U(https://developer.servicenow.com/dev.do#!/reference/api/utah/rest/c_ServiceCatalogAPI#servicecat-GET-items) +""" + +EXAMPLES = r""" +- name: Return all catalogs without categories but with items (brief information) + servicenow.itsm.service_catalog_info: + instance: + host: "{{ sn_host }}" + username: "{{ sn_username }}" + password: "{{ sn_password }}" + categories: false + items: + content: brief + +- name: Return service catalog without categories but with items (brief information) + servicenow.itsm.service_catalog_info: + instance: + host: "{{ sn_host }}" + username: "{{ sn_username }}" + password: "{{ sn_password }}" + sys_id: "{{ service_catalog.sys_id }}" + categories: false + items: + content: brief + +- name: Return service catalog with categories and with items (full information) + servicenow.itsm.service_catalog_info: + instance: + host: "{{ sn_host }}" + username: "{{ sn_username }}" + password: "{{ sn_password }}" + sys_id: "{{ service_catalog.sys_id }}" + categories: true + items: + content: full + +- name: Return service catalog with categories and with all items containing word "iPhone" + servicenow.itsm.service_catalog_info: + instance: + host: "{{ sn_host }}" + username: "{{ sn_username }}" + password: "{{ sn_password }}" + sys_id: "{{ service_catalog.sys_id }}" + categories: true + items: + content: full + query: iPhone +""" + +RETURN = r""" +records: + description: + - List of catalogs. + returned: success + type: List + sample: + "records": [ + { + "categories": [ + { + "description": "Datacenter hardware and services to the support business\n\t\t\tsystems.\n\t\t", + "full_description": null, + "subcategories": [ + { + "sys_id": "d67c446ec0a80165000335aa37eafbc1", + "title": "Services" + } + ], + "sys_id": "803e95e1c3732100fca206e939ba8f2a", + "title": "Infrastructure" + }, + { + "description": "Request for IT services to be performed", + "full_description": null, + "subcategories": [], + "sys_id": "d67c446ec0a80165000335aa37eafbc1", + "title": "Services" + } + ], + "description": "Products and services for the IT department", + "has_categories": true, + "has_items": true, + "items": [ + { + "catalogs": [ + { + "active": true, + "sys_id": "e0d08b13c3330100c8b837659bba8fb4", + "title": "Service Catalog" + }, + { + "active": true, + "sys_id": "742ce428d7211100f2d224837e61036d", + "title": "Technical Catalog" + } + ], + "category": { + "sys_id": "e15706fc0a0a0aa7007fc21e1ab70c2f", + "title": "Can We Help You?" + }, + "description": "
Here you can request a new Knowledge Base to be used. A Knowledge Base can be used to store Knowledge in an organization and anyone can request for a new one to be created.
", + "mandatory_attachment": false, + "name": "Request Knowledge Base", + "order": 0, + "request_method": "", + "short_description": "Request for a Knowledge Base", + "sys_class_name": "sc_cat_item_producer", + "sys_id": "81c887819f203100d8f8700c267fcfb5", + "type": "record_producer" + }, + ], + "sys_id": "742ce428d7211100f2d224837e61036d", + "title": "Technical Catalog" + }] +""" + +from ..module_utils import arguments, client, errors, generic +from ..module_utils.service_catalog import ItemContent, ServiceCatalogClient +from ansible.module_utils.basic import AnsibleModule + + +def get_catalog_info(sc_client, catalog, with_categories=True, with_items=ItemContent.BRIEF): + if with_categories: + catalog.categories = sc_client.get_categories(catalog.sys_id) + + if with_items == ItemContent.NONE: + return catalog + + items = sc_client.get_items(catalog.sys_id) + if with_items == ItemContent.BRIEF: + catalog.items = items + return catalog + + catalog.items = [sc_client.get_item(item.sys_id) for item in items] + + return catalog + + +def validate_params(params): + missing = [] + if "items" in params and "content" not in params["items"]: + missing.append( + 'Missing required subparameter "content" of "items" paramter') + + if not params["items"]["content"]: + missing.append( + 'Missing value for required subparameter "content" of "items"') + elif params["items"]["content"] not in ("brief", "full"): + missing.append( + 'Unknown value {0} for required subparameter "content" of "items"'.format(params["items"]["content"])) + + if "query" in params["items"] and not params["items"]["query"]: + missing.extend('Missing value for "query" subparameter of "items"') + + if missing: + raise errors.ServiceNowError( + "Missing required paramters: {0}". format(", ".join(missing)) + ) + + +def run(module, sc_client): + validate_params(module.params) + + item_information = ItemContent.NONE + if module.params["items"]: + item_information = ItemContent.from_str(module.params["items"]["content"]) + + fetch_categories = module.params["categories"] + + if module.params["sys_id"]: + catalog = get_catalog_info( + sc_client, + sc_client.get_catalog(module.params["sys_id"]), + fetch_categories, + item_information + ) + return [catalog.to_ansible()] + + # fetch all catalogs + catalogs = [] + for catalog in sc_client.get_catalogs(): + catalog = get_catalog_info(sc_client, catalog, fetch_categories, item_information) + catalogs.append(catalog.to_ansible()) + + return catalogs + + +def main(): + module_args = dict( + arguments.get_spec( + "instance", + "sys_id", + ), + categories=dict( + type="bool", + ), + items=dict( + type="dict", + suboptions=dict( + type="dict", + options=dict( + content=dict( + type="str", + choices=["brief", "full"], + required=True + ), + query=dict(type="str") + ) + ) + ), + ) + + module = AnsibleModule( + argument_spec=module_args, + ) + + try: + snow_client = client.Client(**module.params["instance"]) + generic_client = generic.GenericClient(snow_client) + sc_client = ServiceCatalogClient(generic_client) + records = run(module, sc_client) + module.exit_json(changed=False, records=records) + except errors.ServiceNowError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main()