aboutsummaryrefslogtreecommitdiffstats
path: root/app/discover/fetchers/aci
diff options
context:
space:
mode:
authorYaron Yogev <yaronyogev@gmail.com>2017-07-27 09:02:54 +0300
committerYaron Yogev <yaronyogev@gmail.com>2017-07-27 14:56:25 +0300
commit7e83d0876ddb84a45e130eeba28bc40ef53c074b (patch)
tree47d76239ae7658d87c66abd142df92709427e7dd /app/discover/fetchers/aci
parent378ecbd8947589b9cbb39013a0c2e2aa201e03bd (diff)
Calipso initial release for OPNFV
Change-Id: I7210c244b0c10fa80bfa8c77cb86c9d6ddf8bc88 Signed-off-by: Yaron Yogev <yaronyogev@gmail.com>
Diffstat (limited to 'app/discover/fetchers/aci')
-rw-r--r--app/discover/fetchers/aci/__init__.py9
-rw-r--r--app/discover/fetchers/aci/aci_access.py200
-rw-r--r--app/discover/fetchers/aci/aci_fetch_switch_pnic.py91
3 files changed, 300 insertions, 0 deletions
diff --git a/app/discover/fetchers/aci/__init__.py b/app/discover/fetchers/aci/__init__.py
new file mode 100644
index 0000000..b0637e9
--- /dev/null
+++ b/app/discover/fetchers/aci/__init__.py
@@ -0,0 +1,9 @@
+###############################################################################
+# Copyright (c) 2017 Koren Lev (Cisco Systems), Yaron Yogev (Cisco Systems) #
+# and others #
+# #
+# All rights reserved. This program and the accompanying materials #
+# are made available under the terms of the Apache License, Version 2.0 #
+# which accompanies this distribution, and is available at #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+###############################################################################
diff --git a/app/discover/fetchers/aci/aci_access.py b/app/discover/fetchers/aci/aci_access.py
new file mode 100644
index 0000000..836e45d
--- /dev/null
+++ b/app/discover/fetchers/aci/aci_access.py
@@ -0,0 +1,200 @@
+###############################################################################
+# Copyright (c) 2017 Koren Lev (Cisco Systems), Yaron Yogev (Cisco Systems) #
+# and others #
+# #
+# All rights reserved. This program and the accompanying materials #
+# are made available under the terms of the Apache License, Version 2.0 #
+# which accompanies this distribution, and is available at #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+###############################################################################
+import json
+
+import requests
+
+from discover.configuration import Configuration
+from discover.fetcher import Fetcher
+
+
+def aci_config_required(default=None):
+ def decorator(func):
+ def wrapper(self, *args, **kwargs):
+ if not self.aci_enabled:
+ return default
+ return func(self, *args, **kwargs)
+ return wrapper
+ return decorator
+
+
+class AciAccess(Fetcher):
+
+ RESPONSE_FORMAT = "json"
+ cookie_token = None
+
+ def __init__(self):
+ super().__init__()
+ self.configuration = Configuration()
+ self.aci_enabled = self.configuration.get_env_config() \
+ .get('aci_enabled', False)
+ self.aci_configuration = None
+ self.host = None
+ if self.aci_enabled:
+ self.aci_configuration = self.configuration.get("ACI")
+ self.host = self.aci_configuration["host"]
+
+ def get_base_url(self):
+ return "https://{}/api".format(self.host)
+
+ # Unwrap ACI response payload
+ # and return an array of desired fields' values.
+ #
+ # Parameters
+ # ----------
+ #
+ # payload: dict
+ # Full json response payload returned by ACI
+ # *field_names: Tuple[str]
+ # Enumeration of fields that are used to traverse ACI "imdata" array
+ # (order is important)
+ #
+ # Returns
+ # ----------
+ # list
+ # List of unwrapped dictionaries (or primitives)
+ #
+ # Example
+ # ----------
+ # Given payload:
+ #
+ # {
+ # "totalCount": "2",
+ # "imdata": [
+ # {
+ # "aaa": {
+ # "bbb": {
+ # "ccc": "value1"
+ # }
+ # }
+ # },
+ # {
+ # "aaa": {
+ # "bbb": {
+ # "ccc": "value2"
+ # }
+ # }
+ # }
+ # ]
+ # }
+ #
+ # Executing get_objects_by_field_names(payload, "aaa", "bbb")
+ # will yield the following result:
+ #
+ # >>> [{"ccc": "value1"}, {"ccc": "value2"}]
+ #
+ # Executing get_objects_by_field_names(payload, "aaa", "bbb", "ccc")
+ # will yield the following result:
+ #
+ # >>> ["value1", "value2"]
+ #
+ @staticmethod
+ def get_objects_by_field_names(payload, *field_names):
+ results = payload.get("imdata", [])
+ if not results:
+ return []
+
+ for field in field_names:
+ results = [entry[field] for entry in results]
+ return results
+
+ # Set auth tokens in request headers and cookies
+ @staticmethod
+ def _insert_token_into_request(cookies):
+ return dict(cookies, **AciAccess.cookie_token) \
+ if cookies \
+ else AciAccess.cookie_token
+
+ @staticmethod
+ def _set_token(response):
+ tokens = AciAccess.get_objects_by_field_names(response.json(), "aaaLogin", "attributes", "token")
+ token = tokens[0]
+
+ AciAccess.cookie_token = {"APIC-Cookie": token}
+
+ @aci_config_required()
+ def login(self):
+ url = "/".join((self.get_base_url(), "aaaLogin.json"))
+ payload = {
+ "aaaUser": {
+ "attributes": {
+ "name": self.aci_configuration["user"],
+ "pwd": self.aci_configuration["pwd"]
+ }
+ }
+ }
+
+ response = requests.post(url, json=payload, verify=False)
+ response.raise_for_status()
+
+ AciAccess._set_token(response)
+
+ # Refresh token or login if token has expired
+ @aci_config_required()
+ def refresh_token(self):
+ # First time login
+ if not AciAccess.cookie_token:
+ self.login()
+ return
+
+ url = "/".join((self.get_base_url(), "aaaRefresh.json"))
+
+ response = requests.get(url, verify=False)
+
+ # Login again if the token has expired
+ if response.status_code == requests.codes.forbidden:
+ self.login()
+ return
+ # Propagate any other error
+ elif response.status_code != requests.codes.ok:
+ response.raise_for_status()
+
+ AciAccess._set_token(response)
+
+ @aci_config_required(default={})
+ def send_get(self, url, params, headers, cookies):
+ self.refresh_token()
+
+ cookies = self._insert_token_into_request(cookies)
+
+ response = requests.get(url, params=params, headers=headers,
+ cookies=cookies, verify=False)
+ # Let client handle HTTP errors
+ response.raise_for_status()
+
+ return response.json()
+
+ # Search ACI for Managed Objects (MOs) of a specific class
+ @aci_config_required(default=[])
+ def fetch_objects_by_class(self,
+ class_name: str,
+ params: dict = None,
+ headers: dict = None,
+ cookies: dict = None,
+ response_format: str = RESPONSE_FORMAT):
+ url = "/".join((self.get_base_url(),
+ "class", "{cn}.{f}".format(cn=class_name, f=response_format)))
+
+ response_json = self.send_get(url, params, headers, cookies)
+ return self.get_objects_by_field_names(response_json, class_name)
+
+ # Fetch data for a specific Managed Object (MO)
+ @aci_config_required(default=[])
+ def fetch_mo_data(self,
+ dn: str,
+ params: dict = None,
+ headers: dict = None,
+ cookies: dict = None,
+ response_format: str = RESPONSE_FORMAT):
+ url = "/".join((self.get_base_url(), "mo", "topology",
+ "{dn}.{f}".format(dn=dn, f=response_format)))
+
+ response_json = self.send_get(url, params, headers, cookies)
+ return response_json
diff --git a/app/discover/fetchers/aci/aci_fetch_switch_pnic.py b/app/discover/fetchers/aci/aci_fetch_switch_pnic.py
new file mode 100644
index 0000000..a4216ea
--- /dev/null
+++ b/app/discover/fetchers/aci/aci_fetch_switch_pnic.py
@@ -0,0 +1,91 @@
+###############################################################################
+# Copyright (c) 2017 Koren Lev (Cisco Systems), Yaron Yogev (Cisco Systems) #
+# and others #
+# #
+# All rights reserved. This program and the accompanying materials #
+# are made available under the terms of the Apache License, Version 2.0 #
+# which accompanies this distribution, and is available at #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+###############################################################################
+import re
+
+from discover.fetchers.aci.aci_access import AciAccess, aci_config_required
+from utils.inventory_mgr import InventoryMgr
+from utils.util import encode_aci_dn, get_object_path_part
+
+
+class AciFetchSwitchPnic(AciAccess):
+
+ def __init__(self):
+ super().__init__()
+ self.inv = InventoryMgr()
+
+ def fetch_pnics_by_mac_address(self, mac_address):
+ mac_filter = "eq(epmMacEp.addr,\"{}\")".format(mac_address)
+ pnic_filter = "wcard(epmMacEp.ifId, \"eth\")"
+ query_filter = "and({},{})".format(mac_filter, pnic_filter)
+
+ pnics = self.fetch_objects_by_class("epmMacEp",
+ {"query-target-filter": query_filter})
+
+ return [pnic["attributes"] for pnic in pnics]
+
+ def fetch_switch_by_id(self, switch_id):
+ dn = "/".join((switch_id, "sys"))
+ response = self.fetch_mo_data(dn)
+ switch_data = self.get_objects_by_field_names(response, "topSystem", "attributes")
+ return switch_data[0] if switch_data else None
+
+ @aci_config_required(default=[])
+ def get(self, pnic_id):
+ environment = self.get_env()
+ pnic = self.inv.get_by_id(environment=environment, item_id=pnic_id)
+ if not pnic:
+ return []
+ mac_address = pnic.get("mac_address")
+ if not mac_address:
+ return []
+
+ switch_pnics = self.fetch_pnics_by_mac_address(mac_address)
+ if not switch_pnics:
+ return []
+ switch_pnic = switch_pnics[0]
+
+ # Prepare and save switch data in inventory
+ aci_id_match = re.match("topology/(.+)/sys", switch_pnic["dn"])
+ if not aci_id_match:
+ raise ValueError("Failed to fetch switch id from pnic dn: {}"
+ .format(switch_pnic["dn"]))
+
+ aci_switch_id = aci_id_match.group(1)
+ db_switch_id = encode_aci_dn(aci_switch_id)
+ if not self.inv.get_by_id(environment, db_switch_id):
+ switch_data = self.fetch_switch_by_id(aci_switch_id)
+ if not switch_data:
+ self.log.warning("No switch found for switch pnic dn: {}"
+ .format(switch_pnic["dn"]))
+ return []
+
+ switch_json = {
+ "id": db_switch_id,
+ "ip_address": switch_data["address"],
+ "type": "switch",
+ "aci_document": switch_data
+ }
+ # Region name is the same as region id
+ region_id = get_object_path_part(pnic["name_path"], "Regions")
+ region = self.inv.get_by_id(environment, region_id)
+ self.inv.save_inventory_object(o=switch_json, parent=region, environment=environment)
+
+ db_pnic_id = "-".join((db_switch_id,
+ encode_aci_dn(switch_pnic["ifId"]),
+ mac_address))
+ pnic_json = {
+ "id": db_pnic_id,
+ "type": "pnic",
+ "pnic_type": "switch",
+ "mac_address": mac_address,
+ "aci_document": switch_pnic
+ }
+ return [pnic_json]
+