From cd091ba49f3a1ce5c4f92b158a2f45a40b977a55 Mon Sep 17 00:00:00 2001 From: Sumit Jaiswal Date: Wed, 13 Mar 2019 16:22:31 +0530 Subject: [PATCH] PR to include support for Skydive Node and Edge modules with Ansible (#53112) * skydive node and edge module Signed-off-by: Sumit Jaiswal --- .../module_utils/network/skydive/api.py | 209 +++++++++++++++++- .../modules/network/skydive/skydive_edge.py | 198 +++++++++++++++++ .../modules/network/skydive/skydive_node.py | 133 +++++++++++ 3 files changed, 537 insertions(+), 3 deletions(-) create mode 100644 lib/ansible/modules/network/skydive/skydive_edge.py create mode 100644 lib/ansible/modules/network/skydive/skydive_node.py diff --git a/lib/ansible/module_utils/network/skydive/api.py b/lib/ansible/module_utils/network/skydive/api.py index d110a391d0e..951ec9498e7 100644 --- a/lib/ansible/module_utils/network/skydive/api.py +++ b/lib/ansible/module_utils/network/skydive/api.py @@ -27,6 +27,7 @@ # import os +import uuid from ansible.module_utils.six import iteritems from ansible.module_utils.six import iterkeys @@ -34,13 +35,18 @@ from ansible.module_utils._text import to_text from ansible.module_utils.basic import env_fallback try: + from skydive.graph import Node, Edge from skydive.rest.client import RESTClient + from skydive.websocket.client import NodeAddedMsgType, NodeUpdatedMsgType, NodeDeletedMsgType + from skydive.websocket.client import EdgeAddedMsgType, EdgeUpdatedMsgType, EdgeDeletedMsgType + from skydive.websocket.client import WSClient, WSClientDefaultProtocol, WSMessage HAS_SKYDIVE_CLIENT = True except ImportError: HAS_SKYDIVE_CLIENT = False # defining skydive constants SKYDIVE_GREMLIN_QUERY = 'G.V().Has' +SKYDIVE_GREMLIN_EDGE_QUERY = 'G.E().Has' SKYDIVE_PROVIDER_SPEC = { 'endpoint': dict(fallback=(env_fallback, ['SKYDIVE_ENDPOINT'])), @@ -51,11 +57,13 @@ SKYDIVE_PROVIDER_SPEC = { } -class skydive_restclient(object): - ''' Base class for implementing Skydive Rest API ''' +class skydive_client_check(object): + """ Base class for implementing Skydive Rest API """ + provider_spec = {'provider': dict(type='dict', options=SKYDIVE_PROVIDER_SPEC)} def __init__(self, **kwargs): + ''' Base class for implementing Skydive Rest API ''' if not HAS_SKYDIVE_CLIENT: raise Exception('skydive-client is required but does not appear ' 'to be installed. It can be installed using the ' @@ -74,6 +82,135 @@ class skydive_restclient(object): env = ('SKYDIVE_%s' % key).upper() if env in os.environ: kwargs[key] = os.environ.get(env) + + +class skydive_inject_protocol(object): + """ Implements inject protocol for node and edge modules """ + + def onOpen(self): + module = self.factory.kwargs["module"] + params = self.factory.kwargs["params"] + result = self.factory.kwargs["result"] + if "node1" and "node2" in self.factory.kwargs: + node1 = self.factory.kwargs["node1"] + node2 = self.factory.kwargs["node2"] + + if module.check_mode: + self.stop() + return + try: + host = params["host"] + if params["metadata"]: + metadata = module._check_type_dict(params["metadata"]) + else: + metadata = {} + if "node_type" in params: + metadata["Name"] = params["name"] + metadata["Type"] = params["node_type"] + seed = params["seed"] + if not seed: + seed = "%s:%s" % (params["name"], params["node_type"]) + if module.params['state'] == 'present' or module.params['state'] == 'update': + uid = str(uuid.uuid5(uuid.NAMESPACE_OID, seed)) + node = Node(uid, host, metadata=metadata) + if module.params['state'] == 'present': + msg = WSMessage("Graph", NodeAddedMsgType, node) + else: + msg = WSMessage("Graph", NodeUpdatedMsgType, node) + else: + uid = params['id'] + node = Node(uid, host, metadata=metadata) + msg = WSMessage("Graph", NodeDeletedMsgType, node) + elif "relation_type" in params: + metadata["RelationType"] = params["relation_type"] + if module.params['state'] == 'present' or module.params['state'] == 'update': + uid = str(uuid.uuid5(uuid.NAMESPACE_OID, "%s:%s:%s" % + (node1, node2, params["relation_type"]))) + edge = Edge(uid, host, node1, node2, metadata=metadata) + if module.params['state'] == 'present': + msg = WSMessage("Graph", EdgeAddedMsgType, edge) + else: + msg = WSMessage("Graph", EdgeUpdatedMsgType, edge) + else: + uid = module.params['id'] + edge = Edge(uid, host, node1, node2, metadata=metadata) + msg = WSMessage("Graph", EdgeDeletedMsgType, edge) + + self.sendWSMessage(msg) + if uid: + result["UUID"] = uid + result["changed"] = True + except Exception as e: + module.fail_json( + msg='Error during topology update %s' % e, **result) + finally: + self.stop() + + +class skydive_wsclient(skydive_client_check): + """ Base class for implementing Skydive Websocket API """ + + def __init__(self, module, **kwargs): + super(skydive_wsclient, self).__init__(**kwargs) + + class skydive_full_inject_protocol(skydive_inject_protocol, WSClientDefaultProtocol): + pass + kwargs['scheme'] = "ws" + if 'ssl' in kwargs: + if kwargs['ssl']: + kwargs['scheme'] = "wss" + if 'insecure' not in kwargs: + kwargs['insecure'] = False + scheme = kwargs['scheme'] + self.result = dict(changed=False) + if "node_type" in module.params: + self.wsclient_object = WSClient("ansible-" + str(os.getpid()) + "-" + module.params['host'], + "%s://%s/ws/publisher" % (scheme, kwargs["endpoint"]), + protocol=type('skydive_full_inject_protocol', (skydive_inject_protocol, + WSClientDefaultProtocol), dict()), + persistent=True, + insecure=kwargs["insecure"], + username=kwargs["username"], + password=kwargs["password"], + module=module, + params=module.params, + result=self.result) + elif "relation_type" in module.params: + self.parent_node = self.get_node_id(module.params['parent_node']) + self.child_node = self.get_node_id(module.params['child_node']) + + self.wsclient_object = WSClient("ansible-" + str(os.getpid()) + "-" + module.params['host'], + "%s://%s/ws/publisher" % (scheme, kwargs["endpoint"]), + protocol=type('skydive_full_inject_protocol', (skydive_inject_protocol, + WSClientDefaultProtocol), dict()), + persistent=True, + insecure=kwargs["insecure"], + username=kwargs["username"], + password=kwargs["password"], + module=module, + params=module.params, + node1=self.parent_node, + node2=self.child_node, + result=self.result) + + def get_node_id(self, node_selector): + """ Checks if Gremlin expresssion is passed as input to get the nodes UUID """ + if node_selector.startswith("G.") or node_selector.startswith("g."): + nodes = self.restclient_object.lookup_nodes(node_selector) + if len(nodes) == 0: + raise self.module.fail_json(msg=to_text("Node not found: {0}".format(node_selector))) + elif len(nodes) > 1: + raise self.module.fail_json( + msg=to_text("Node selection should return only one node: {0}".format(node_selector))) + return str(nodes[0].id) + return node_selector + + +class skydive_restclient(skydive_client_check): + """ Base class for implementing Skydive Rest API """ + + def __init__(self, **kwargs): + super(skydive_restclient, self).__init__(**kwargs) kwargs['scheme'] = "http" if 'ssl' in kwargs: if kwargs['ssl']: @@ -88,6 +225,8 @@ class skydive_restclient(object): class skydive_lookup(skydive_restclient): + """ Implements Skydive Lookup queries """ + provider_spec = {'provider': dict(type='dict', options=SKYDIVE_PROVIDER_SPEC)} def __init__(self, provider): @@ -107,7 +246,8 @@ class skydive_lookup(skydive_restclient): class skydive_flow_capture(skydive_restclient): - ''' Implements Skydive Flow capture modules ''' + """ Implements Skydive Flow capture modules """ + def __init__(self, module): self.module = module provider = module.params['provider'] @@ -161,3 +301,66 @@ class skydive_flow_capture(skydive_restclient): result['changed'] = True return result + + +class skydive_node(skydive_wsclient, skydive_restclient): + """ Implements Skydive Node modules """ + + def __init__(self, module): + self.module = module + provider = module.params['provider'] + super(skydive_node, self).__init__(self.module, **provider) + + def run(self): + try: + lookup_query = SKYDIVE_GREMLIN_QUERY + "('Name', '{0}', 'Type', '{1}')".format(self.module.params['name'], + self.module.params['node_type']) + node_exists = self.restclient_object.lookup_nodes(lookup_query) + + if not node_exists and self.module.params['state'] == 'present': + self.wsclient_object.connect() + self.wsclient_object.start() + elif len(node_exists) > 0 and self.module.params['state'] == 'update': + self.wsclient_object.connect() + self.wsclient_object.start() + elif len(node_exists) > 0 and self.module.params['state'] == 'absent': + self.module.params['id'] = node_exists[0].__dict__['id'] + self.wsclient_object.connect() + self.wsclient_object.start() + except Exception as e: + self.module.fail_json(msg=to_text(e)) + return self.result + + +class skydive_edge(skydive_wsclient, skydive_restclient): + """ Implements Skydive Edge modules """ + + def __init__(self, module): + self.module = module + provider = module.params['provider'] + + super(skydive_edge, self).__init__(self.module, **provider) + + def run(self): + try: + edge_exists = False + edge_query = SKYDIVE_GREMLIN_EDGE_QUERY + "('Parent', '{0}', 'Child', '{1}')".format(self.parent_node, + self.child_node) + query_result = self.restclient_object.lookup_edges(edge_query) + if query_result: + query_result = query_result[0].__dict__ + edge_exists = True + + if not edge_exists and self.module.params['state'] == 'present': + self.wsclient_object.connect() + self.wsclient_object.start() + elif edge_exists and self.module.params['state'] == 'update': + self.wsclient_object.connect() + self.wsclient_object.start() + elif edge_exists and self.module.params['state'] == 'absent': + self.module.params['id'] = query_result['id'] + self.wsclient_object.connect() + self.wsclient_object.start() + except Exception as e: + self.module.fail_json(msg=to_text(e)) + return self.result diff --git a/lib/ansible/modules/network/skydive/skydive_edge.py b/lib/ansible/modules/network/skydive/skydive_edge.py new file mode 100644 index 00000000000..07efb5f06d0 --- /dev/null +++ b/lib/ansible/modules/network/skydive/skydive_edge.py @@ -0,0 +1,198 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2019, Ansible by Red Hat, inc +# 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': 'network'} + +DOCUMENTATION = """ +--- +module: skydive_edge +version_added: "2.8" +author: + - "Sumit Jaiswal (@sjaiswal)" +short_description: Module to add edges to Skydive topology +description: + - This module handles setting up edges between two nodes based on the + relationship type to the Skydive topology. +requirements: + - skydive-client +extends_documentation_fragment: skydive +options: + parent_node: + description: + - To defined the first node of the link, it can be either an ID or + a gremlin expression + required: true + child_node: + description: + - To defined the second node of the link, it can be either an ID or + a gremlin expression + required: true + relation_type: + description: + - To define relation type of the node I(ownership, layer2, layer3). + required: true + host: + description: + - To define the host of the node. + default: "" + required: False + metadata: + description: + - To define metadata for the edge. + required: false + state: + description: + - State of the Skydive Edge. If value is I(present) new edge + will be created else if it is I(absent) it will be deleted. + default: present + choices: + - present + - absent +""" + +EXAMPLES = """ +- name: create tor + skydive_node: + name: 'TOR' + node_type: "fabric" + seed: TOR + metadata: + Model: Cisco xxxx + provider: + endpoint: localhost:8082 + username: admin + password: admin + register: tor_result + +- name: create port 1 + skydive_node: + name: 'PORT1' + node_type: 'fabric' + seed: PORT1 + provider: + endpoint: localhost:8082 + username: admin + password: admin + register: port1_result + +- name: create port 2 + skydive_node: + name: 'PORT2' + node_type: 'fabric' + seed: PORT2 + provider: + endpoint: localhost:8082 + username: admin + password: admin + register: port2_result + +- name: link node tor and port 1 + skydive_edge: + parent_node: "{{ tor_result.UUID }}" + child_node: "{{ port1_result.UUID }}" + relation_type: ownership + state: present + provider: + endpoint: localhost:8082 + username: admin + password: admin + +- name: link node tor and port 2 + skydive_edge: + parent_node: "{{ tor_result.UUID }}" + child_node: "{{ port2_result.UUID }}" + relation_type: ownership + state: present + provider: + endpoint: localhost:8082 + username: admin + password: admin + +- name: update link node tor and port 1 relation + skydive_edge: + parent_node: "{{ tor_result.UUID }}" + child_node: "{{ port2_result.UUID }}" + relation_type: layer2 + state: upadte + provider: + endpoint: localhost:8082 + username: admin + password: admin + +- name: Unlink tor and port 2 + skydive_edge: + parent_node: "{{ tor_result.UUID }}" + child_node: "{{ port2_result.UUID }}" + relation_type: ownership + state: absent + provider: + endpoint: localhost:8082 + username: admin + password: admin + +- name: link tor and port 2 via Gremlin expression + skydive_edge: + parent_node: G.V().Has('Name', 'TOR') + child_node: G.V().Has('Name', 'PORT2') + relation_type: ownership + state: present + provider: + endpoint: localhost:8082 + username: admin + password: admin + +- name: Unlink tor and port 2 via Gremlin expression + skydive_edge: + parent_node: G.V().Has('Name', 'TOR') + child_node: G.V().Has('Name', 'PORT2') + relation_type: ownership + state: absent + provider: + endpoint: localhost:8082 + username: admin + password: admin +""" + +RETURN = """ # """ + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.skydive.api import skydive_edge + + +def main(): + ''' Main entry point for module execution + ''' + ib_spec = dict( + relation_type=dict(type='str', required=True), + parent_node=dict(type='str', required=True), + child_node=dict(type='str', required=True), + host=dict(type='str', default=""), + metadata=dict(type='dict', default=dict()) + ) + + argument_spec = dict( + provider=dict(required=False), + state=dict(default='present', choices=['present', 'absent']) + ) + + argument_spec.update(ib_spec) + argument_spec.update(skydive_edge.provider_spec) + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + skydive_obj = skydive_edge(module) + result = skydive_obj.run() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/network/skydive/skydive_node.py b/lib/ansible/modules/network/skydive/skydive_node.py new file mode 100644 index 00000000000..59ba788998f --- /dev/null +++ b/lib/ansible/modules/network/skydive/skydive_node.py @@ -0,0 +1,133 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2019, Ansible by Red Hat, inc +# 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': 'network'} + +DOCUMENTATION = """ +--- +module: skydive_node +version_added: "2.8" +author: + - "Sumit Jaiswal (@sjaiswal)" +short_description: Module which add nodes to Skydive topology +description: + - This module handles adding node to the Skydive topology. +requirements: + - skydive-client +extends_documentation_fragment: skydive +options: + name: + description: + - To define name for the node. + required: true + node_type: + description: + - To define type for the node. + required: true + host: + description: + - To define host for the node. + required: false + seed: + description: + - used to generate the UUID of the node + default: "" + metadata: + description: + - To define metadata for the node. + required: false + state: + description: + - State of the Skydive Node. If value is I(present) new node + will be created else if it is I(absent) it will be deleted. + default: present + choices: + - present + - update + - absent +""" + +EXAMPLES = """ +- name: create tor node + skydive_node: + name: TOR + node_type: fabric + seed: TOR1 + metadata: + Model: Cisco 5300 + state: present + provider: + endpoint: localhost:8082 + username: admin + password: admin + +- name: update tor node + skydive_node: + name: TOR + node_type: host + seed: TOR1 + metadata: + Model: Cisco 3400 + state: update + provider: + endpoint: localhost:8082 + username: admin + password: admin + +- name: Delete the tor node + skydive_node: + name: TOR + node_type: host + seed: TOR1 + metadata: + Model: Cisco 3400 + state: absent + provider: + endpoint: localhost:8082 + username: admin + password: admin +""" + +RETURN = """ # """ + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.skydive.api import skydive_node + + +def main(): + ''' Main entry point for module execution + ''' + ib_spec = dict( + name=dict(required=True, ib_req=True), + node_type=dict(required=True, ib_req=True), + host=dict(required=False, ib_req=True, default=""), + seed=dict(required=False, ib_req=True, default=""), + metadata=dict(required=False, ib_req=True, default=dict()) + ) + + argument_spec = dict( + provider=dict(required=False), + state=dict(default='present', choices=['present', 'update', 'absent']) + ) + + argument_spec.update(ib_spec) + argument_spec.update(skydive_node.provider_spec) + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + skydive_obj = skydive_node(module) + result = skydive_obj.run() + module.exit_json(**result) + + +if __name__ == '__main__': + main()