From 7e83d0876ddb84a45e130eeba28bc40ef53c074b Mon Sep 17 00:00:00 2001 From: Yaron Yogev Date: Thu, 27 Jul 2017 09:02:54 +0300 Subject: Calipso initial release for OPNFV Change-Id: I7210c244b0c10fa80bfa8c77cb86c9d6ddf8bc88 Signed-off-by: Yaron Yogev --- app/discover/fetchers/aci/__init__.py | 9 + app/discover/fetchers/aci/aci_access.py | 200 +++++++++++++++++++++ app/discover/fetchers/aci/aci_fetch_switch_pnic.py | 91 ++++++++++ 3 files changed, 300 insertions(+) create mode 100644 app/discover/fetchers/aci/__init__.py create mode 100644 app/discover/fetchers/aci/aci_access.py create mode 100644 app/discover/fetchers/aci/aci_fetch_switch_pnic.py (limited to 'app/discover/fetchers/aci') 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] + -- cgit 1.2.3-korg