class MDNSInterface(object): def __init__(self, interfaceIp): self.logger = Logger('MDNSInterface') self.ip = interfaceIp self._establish_interface() def _establish_interface(self): try: self.zeroconf = Zeroconf([self.ip]) except socket.error: msg = "Could not find interface with IP {}".format(self.ip) self.logger.writeError(msg) raise InterfaceNotFoundException(msg) def registerService(self, info): self.zeroconf.register_service(info) def unregisterService(self, info): self.zeroconf.unregister_service(info) def close(self): self.zeroconf.close()
class GarbageCollect(object): parent_tab = { 'devices': [('nodes', 'node_id')], 'senders': [('devices', 'device_id')], 'receivers': [('devices', 'device_id')], 'sources': [('devices', 'device_id')], 'flows': [('devices', 'device_id'), ('sources', 'source_id')] } def __init__(self, registry, identifier, logger=None, interval=INTERVAL): """ interval Number of seconds between checks / collections. An interval of '0' means 'never check'. """ self.registry = registry self.logger = Logger("garbage_collect", logger) self.identifier = identifier if interval > 0: gevent.spawn_later(interval, self.garbage_collect) def garbage_collect(self): # Check to see if garbage collection hasn't been done recently (by another aggregator) # Uses ETCD's prevExist=false function # See https://github.com/coreos/etcd/blob/master/Documentation/api.md#atomic-compare-and-swap try: flag = self.registry.put_garbage_collection_flag( host=self.identifier, ttl=LOCK_TIMEOUT) if flag.status_code != 201: self.logger.writeDebug( "Not collecting - another collector has recently collected" ) return # Kick off a collection with a specified timeout. try: with gevent.Timeout(TIMEOUT, TooLong): self._collect() finally: self.logger.writeDebug("remove flag") self._remove_flag() except Exception as e: self.logger.writeError( "Could not write garbage collect flag: {}".format(e)) finally: # Always schedule another gevent.spawn_later(INTERVAL, self.garbage_collect) self.logger.writeDebug("scheduled...") def _collect(self): try: self.logger.writeDebug("Collecting: {}".format(self.identifier)) # create list of nodes still alive alive_nodes = [] health_dict = self.registry.get_healths() for h in health_dict.get('/health', {}).keys(): node_name = h.split('/')[-1] alive_nodes.append(node_name) # TODO: GETs... maybe getting the whole response in one go is better? # Maybe doing these async is a good idea? For now, this suffices. all_types = [ "nodes", "devices", "senders", "receivers", "sources", "flows" ] resources = { rtype: self.registry.get_all(rtype) for rtype in all_types } # Get a flat list of (type, resource) pairs for existing resources # TODO: combine with above all_resources = [] for res_type, res in resources.items(): all_resources += [(res_type, x) for x in res] # Initialise the removal queue with any dead nodes nodes = [x.strip('/') for x in self.registry.getresources("nodes")] # TODO: already have this above... kill_q = [('nodes', node_id) for node_id in nodes if node_id not in alive_nodes] # Create a list of (type, id) pairs of resources that should be removed. to_kill = [] # Find orphaned resources kill_q += self.__find_dead_resources(all_resources, to_kill) # Process the removal queue. while kill_q: gevent.sleep(0.0) # Add these resources to the list of removals to_kill += kill_q # Reduce search space; this resource can never parent another # This proves to be faster in the long run. all_resources = [ x for x in all_resources if (x[0], x[1]['id']) not in to_kill ] # Look through remaining resources and get a new kill_q kill_q = self.__find_dead_resources(all_resources, to_kill) for resource_type, resource_id in to_kill: self.logger.writeInfo("removing resource: {}/{}".format( resource_type, resource_id)) self.registry.delete(resource_type, resource_id) except self.registry.RegistryUnavailable: self.logger.writeWarning("registry unavailable") except TooLong: self.logger.writeWarning("took too long") except Exception as e: self.logger.writeError("unhandled exception: {}".format(e)) def __find_dead_resources(self, all_resources, to_kill): def is_alive(parent_def): if parent_def in to_kill: return False parent_type, parent_id = parent_def found_parent = next( (x for x in all_resources if x[0] == parent_type and x[1]['id'] == parent_id), None) return found_parent is not None # Build a list of resource to remove kill_q = [] # Look through all remaining resources for child_type, child in all_resources: # We need never consider nodes; they should have already been marked. if child_type == "nodes": continue child_id = child['id'] # Get parent for child. There is only ever one; anything with multiple # parent entries in the parent table has multiple entries for backward # compatibility, in order strongest->weakest. parents = [(parent_type, child.get(parent_key)) for parent_type, parent_key in self.parent_tab.get( child_type, (None, None))] parent = next((x for x in parents if x[1] is not None), None) if parent is None or not is_alive(parent): kill_q.append((child_type, child_id)) return kill_q def _remove_flag(self): try: self.registry.delete_raw("garbage_collection") except Exception as e: self.logger.writeWarning("Could not remove flag: {}".format(e))
class Aggregator(object): """This class serves as a proxy for the distant aggregation service running elsewhere on the network. It will search out aggregators and locate them, falling back to other ones if the one it is connected to disappears, and resending data as needed.""" def __init__(self, logger=None, mdns_updater=None): self.logger = Logger("aggregator_proxy", logger) self.mdnsbridge = IppmDNSBridge(logger=self.logger) self.aggregator = "" self.registration_order = [ "device", "source", "flow", "sender", "receiver" ] self._mdns_updater = mdns_updater # 'registered' is a local mirror of aggregated items. There are helper methods # for manipulating this below. self._registered = { 'node': None, 'registered': False, 'entities': { 'resource': {} } } self._running = True self._reg_queue = gevent.queue.Queue() self.heartbeat_thread = gevent.spawn(self._heartbeat) self.queue_thread = gevent.spawn(self._process_queue) # The heartbeat thread runs in the background every five seconds. # If when it runs the Node is believed to be registered it will perform a heartbeat def _heartbeat(self): self.logger.writeDebug("Starting heartbeat thread") while self._running: heartbeat_wait = 5 if not self._registered["registered"]: self._process_reregister() elif self._registered["node"]: # Do heartbeat try: self.logger.writeDebug( "Sending heartbeat for Node {}".format( self._registered["node"]["data"]["id"])) self._SEND( "POST", "/health/nodes/" + self._registered["node"]["data"]["id"]) except InvalidRequest as e: if e.status_code == 404: # Re-register self.logger.writeWarning( "404 error on heartbeat. Marking Node for re-registration" ) self._registered["registered"] = False if (self._mdns_updater is not None): self._mdns_updater.inc_P2P_enable_count() else: # Client side error. Report this upwards via exception, but don't resend self.logger.writeError( "Unrecoverable error code {} received from Registration API on heartbeat" .format(e.status_code)) self._running = False except: # Re-register self.logger.writeWarning( "Unexpected error on heartbeat. Marking Node for re-registration" ) self._registered["registered"] = False else: self._registered["registered"] = False if (self._mdns_updater is not None): self._mdns_updater.inc_P2P_enable_count() while heartbeat_wait > 0 and self._running: gevent.sleep(1) heartbeat_wait -= 1 self.logger.writeDebug("Stopping heartbeat thread") # Provided the Node is believed to be correctly registered, hand off a single request to the SEND method # On client error, clear the resource from the local mirror # On other error, mark Node as unregistered and trigger re-registration def _process_queue(self): self.logger.writeDebug("Starting HTTP queue processing thread") while self._running or ( self._registered["registered"] and not self._reg_queue.empty() ): # Checks queue not empty before quitting to make sure unregister node gets done if not self._registered["registered"] or self._reg_queue.empty(): gevent.sleep(1) else: try: queue_item = self._reg_queue.get() namespace = queue_item["namespace"] res_type = queue_item["res_type"] res_key = queue_item["key"] if queue_item["method"] == "POST": if res_type == "node": data = self._registered["node"] try: self.logger.writeInfo( "Attempting registration for Node {}". format(self._registered["node"]["data"] ["id"])) self._SEND("POST", "/{}".format(namespace), data) self._SEND( "POST", "/health/nodes/" + self._registered["node"]["data"]["id"]) self._registered["registered"] = True if self._mdns_updater is not None: self._mdns_updater.P2P_disable() except Exception as ex: self.logger.writeWarning( "Error registering Node: %r" % (traceback.format_exc(), )) elif res_key in self._registered["entities"][ namespace][res_type]: data = self._registered["entities"][namespace][ res_type][res_key] try: self._SEND("POST", "/{}".format(namespace), data) except InvalidRequest as e: self.logger.writeWarning( "Error registering {} {}: {}".format( res_type, res_key, e)) self.logger.writeWarning( "Request data: {}".format( self._registered["entities"][namespace] [res_type][res_key])) del self._registered["entities"][namespace][ res_type][res_key] elif queue_item["method"] == "DELETE": translated_type = res_type + 's' try: self._SEND( "DELETE", "/{}/{}/{}".format(namespace, translated_type, res_key)) except InvalidRequest as e: self.logger.writeWarning( "Error deleting resource {} {}: {}".format( translated_type, res_key, e)) else: self.logger.writeWarning( "Method {} not supported for Registration API interactions" .format(queue_item["method"])) except Exception as e: self._registered["registered"] = False if (self._mdns_updater is not None): self._mdns_updater.P2P_disable() self.logger.writeDebug("Stopping HTTP queue processing thread") # Queue a request to be processed. Handles all requests except initial Node POST which is done in _process_reregister def _queue_request(self, method, namespace, res_type, key): self._reg_queue.put({ "method": method, "namespace": namespace, "res_type": res_type, "key": key }) # Register 'resource' type data including the Node # NB: Node registration is managed by heartbeat thread so may take up to 5 seconds! def register(self, res_type, key, **kwargs): self.register_into("resource", res_type, key, **kwargs) # Unregister 'resource' type data including the Node def unregister(self, res_type, key): self.unregister_from("resource", res_type, key) # General register method for 'resource' types def register_into(self, namespace, res_type, key, **kwargs): data = kwargs send_obj = {"type": res_type, "data": data} if 'id' not in send_obj["data"]: self.logger.writeWarning( "No 'id' present in data, using key='{}': {}".format( key, data)) send_obj["data"]["id"] = key if namespace == "resource" and res_type == "node": # Handle special Node type self._registered["node"] = send_obj else: self._add_mirror_keys(namespace, res_type) self._registered["entities"][namespace][res_type][key] = send_obj self._queue_request("POST", namespace, res_type, key) # General unregister method for 'resource' types def unregister_from(self, namespace, res_type, key): if namespace == "resource" and res_type == "node": # Handle special Node type self._registered["node"] = None elif res_type in self._registered["entities"][namespace]: self._add_mirror_keys(namespace, res_type) if key in self._registered["entities"][namespace][res_type]: del self._registered["entities"][namespace][res_type][key] self._queue_request("DELETE", namespace, res_type, key) # Deal with missing keys in local mirror def _add_mirror_keys(self, namespace, res_type): if namespace not in self._registered["entities"]: self._registered["entities"][namespace] = {} if res_type not in self._registered["entities"][namespace]: self._registered["entities"][namespace][res_type] = {} # Re-register just the Node, and queue requests in order for other resources def _process_reregister(self): if self._registered.get("node", None) is None: self.logger.writeDebug("No node registered, re-register returning") return try: self.logger.writeDebug( "Clearing old Node from API prior to re-registration") self._SEND( "DELETE", "/resource/nodes/" + self._registered["node"]["data"]["id"]) except InvalidRequest as e: # 404 etc is ok self.logger.writeInfo( "Invalid request when deleting Node prior to registration: {}". format(e)) except Exception as ex: # Server error is bad, no point continuing self.logger.writeError("Aborting Node re-register! {}".format(ex)) return self._registered["registered"] = False if (self._mdns_updater is not None): self._mdns_updater.inc_P2P_enable_count() # Drain the queue while not self._reg_queue.empty(): try: self._reg_queue.get(block=False) except gevent.queue.Queue.Empty: break try: # Register the node, and immediately heartbeat if successful to avoid race with garbage collect. self.logger.writeInfo( "Attempting re-registration for Node {}".format( self._registered["node"]["data"]["id"])) self._SEND("POST", "/resource", self._registered["node"]) self._SEND( "POST", "/health/nodes/" + self._registered["node"]["data"]["id"]) self._registered["registered"] = True if self._mdns_updater is not None: self._mdns_updater.P2P_disable() except Exception as e: self.logger.writeWarning("Error re-registering Node: {}".format(e)) self.aggregator == "" # Fallback to prevent us getting stuck if the Reg API issues a 4XX error incorrectly return # Re-register items that must be ordered # Re-register things we have in the local cache. # "namespace" is e.g. "resource" # "entities" are the things associated under that namespace. for res_type in self.registration_order: for namespace, entities in self._registered["entities"].items(): if res_type in entities: self.logger.writeInfo( "Ordered re-registration for type: '{}' in namespace '{}'" .format(res_type, namespace)) for key in entities[res_type]: self._queue_request("POST", namespace, res_type, key) # Re-register everything else # Re-register things we have in the local cache. # "namespace" is e.g. "resource" # "entities" are the things associated under that namespace. for namespace, entities in self._registered["entities"].items(): for res_type in entities: if res_type not in self.registration_order: self.logger.writeInfo( "Unordered re-registration for type: '{}' in namespace '{}'" .format(res_type, namespace)) for key in entities[res_type]: self._queue_request("POST", namespace, res_type, key) # Stop the Aggregator object running def stop(self): self.logger.writeDebug("Stopping aggregator proxy") self._running = False self.heartbeat_thread.join() self.queue_thread.join() # Handle sending all requests to the Registration API, and searching for a new 'aggregator' if one fails def _SEND(self, method, url, data=None): if self.aggregator == "": self.aggregator = self.mdnsbridge.getHref(REGISTRATION_MDNSTYPE) if data is not None: data = json.dumps(data) url = AGGREGATOR_APINAMESPACE + "/" + AGGREGATOR_APINAME + "/" + AGGREGATOR_APIVERSION + url for i in range(0, 3): if self.aggregator == "": self.logger.writeWarning( "No aggregator available on the network or mdnsbridge unavailable" ) raise NoAggregator(self._mdns_updater) self.logger.writeDebug("{} {}".format( method, urljoin(self.aggregator, url))) # We give a long(ish) timeout below, as the async request may succeed after the timeout period # has expired, causing the node to be registered twice (potentially at different aggregators). # Whilst this isn't a problem in practice, it may cause excessive churn in websocket traffic # to web clients - so, sacrifice a little timeliness for things working as designed the # majority of the time... try: if nmoscommonconfig.config.get('prefer_ipv6', False) == False: R = requests.request(method, urljoin(self.aggregator, url), data=data, timeout=1.0) else: R = requests.request(method, urljoin(self.aggregator, url), data=data, timeout=1.0, proxies={'http': ''}) if R is None: # Try another aggregator self.logger.writeWarning( "No response from aggregator {}".format( self.aggregator)) elif R.status_code in [200, 201]: if R.headers.get( "content-type", "text/plain").startswith("application/json"): return R.json() else: return R.content elif R.status_code == 204: return elif (R.status_code / 100) == 4: self.logger.writeWarning( "{} response from aggregator: {} {}".format( R.status_code, method, urljoin(self.aggregator, url))) raise InvalidRequest(R.status_code, self._mdns_updater) else: self.logger.writeWarning( "Unexpected status from aggregator {}: {}, {}".format( self.aggregator, R.status_code, R.content)) except requests.exceptions.RequestException as ex: # Log a warning, then let another aggregator be chosen self.logger.writeWarning("{} from aggregator {}".format( ex, self.aggregator)) # This aggregator is non-functional self.aggregator = self.mdnsbridge.getHref(REGISTRATION_MDNSTYPE) self.logger.writeInfo("Updated aggregator to {} (try {})".format( self.aggregator, i)) raise TooManyRetries(self._mdns_updater)
class FacadeRegistry(object): def __init__(self, resources, aggregator, mdns_updater, node_id, node_data, logger=None): # `node_data` must be correctly structured self.permitted_resources = resources self.services = {} self.clocks = {"clk0": {"name": "clk0", "ref_type": "internal"}} self.aggregator = aggregator self.mdns_updater = mdns_updater self.node_id = node_id assert "interfaces" in node_data # Check data conforms to latest supported API version self.node_data = node_data self.logger = Logger("facade_registry", logger) def modify_node(self, **kwargs): for key in kwargs.keys(): if key in self.node_data: self.node_data[key] = kwargs[key] self.update_node() def update_node(self): self.node_data["services"] = [] for service_name in self.services: href = None if self.services[service_name]["href"]: if self.services[service_name]["proxy_path"]: href = self.node_data["href"] + self.services[service_name]["proxy_path"] self.node_data["services"].append({ "href": href, "type": self.services[service_name]["type"], "authorization": self.services[service_name]["authorization"] }) self.node_data["clocks"] = list(itervalues(self.clocks)) self.node_data["version"] = str(ptptime.ptp_detail()[0]) + ":" + str(ptptime.ptp_detail()[1]) try: self.aggregator.register("node", self.node_id, **self.preprocess_resource("node", self.node_data["id"], self.node_data, NODE_REGVERSION)) except Exception as e: self.logger.writeError("Exception re-registering node: {}".format(e)) def register_service(self, name, srv_type, pid, href=None, proxy_path=None, authorization=False): if name in self.services: return RES_EXISTS self.services[name] = { "heartbeat": time.time(), "resource": {}, # Registered resources live under here "control": {}, # Registered device controls live under here "pid": pid, "href": href, "proxy_path": proxy_path, "type": srv_type, "authorization": authorization } for resource_name in self.permitted_resources: self.services[name]["resource"][resource_name] = {} self.update_node() return RES_SUCCESS def update_service(self, name, pid, href=None, proxy_path=None): if name not in self.services: return RES_NOEXISTS if self.services[name]["pid"] != pid: return RES_UNAUTHORISED self.services[name]["heartbeat"] = time.time() self.services[name]["href"] = href self.services[name]["proxy_path"] = proxy_path self.update_node() return RES_SUCCESS def unregister_service(self, name, pid): if name not in self.services: return RES_NOEXISTS if self.services[name]["pid"] != pid: return RES_UNAUTHORISED for namespace in ["resource", "control"]: for type in self.services[name][namespace].keys(): for key in self.services[name][namespace][type].keys(): if namespace == "control": self._register(name, "control", pid, type, "remove", self.services[name][namespace][type][key]) else: self._unregister(name, namespace, pid, type, key) self.services.pop(name, None) self.update_node() return RES_SUCCESS def heartbeat_service(self, name, pid): if name not in self.services: return RES_NOEXISTS if self.services[name]["pid"] != pid: return RES_UNAUTHORISED self.services[name]["heartbeat"] = time.time() return RES_SUCCESS def cleanup_services(self): timed_out = time.time() - HEARTBEAT_TIMEOUT for name in list(self.services.keys()): if self.services[name]["heartbeat"] < timed_out: self.unregister_service(name, self.services[name]["pid"]) def register_resource(self, service_name, pid, type, key, value): if type not in self.permitted_resources: return RES_UNSUPPORTED return self._register(service_name, "resource", pid, type, key, value) def register_control(self, service_name, pid, device_id, control_data): return self._register( service_name=service_name, namespace="control", pid=pid, type=device_id, key="add", value=control_data ) def _register(self, service_name, namespace, pid, type, key, value): if namespace != "control": if "max_api_version" not in value: self.logger.writeWarning( "Service {}: Registration without valid api version specified".format(service_name) ) value["max_api_version"] = "v1.0" elif api_ver_compare(value["max_api_version"], NODE_REGVERSION) < 0: self.logger.writeWarning( "Trying to register resource with api version too low: '{}' : {}".format(key, json.dumps(value)) ) if service_name not in self.services: return RES_NOEXISTS if not self.services[service_name]["pid"] == pid: return RES_UNAUTHORISED if key == "00000000-0000-0000-0000-000000000000": return RES_OTHERERROR # Add a node_id to those resources which need one if type == 'device': value['node_id'] = self.node_id if namespace == "control": if type not in self.services[service_name][namespace]: # 'type' is the Device ID in this case self.services[service_name][namespace][type] = {} if key == "add": # Register self.services[service_name][namespace][type][value["href"]] = value else: # Unregister self.services[service_name][namespace][type].pop(value["href"], None) # Reset the parameters below to force re-registration of the corresponding Device namespace = "resource" key = type # Device ID type = "device" value = None for name in self.services: # Find the service which registered the Device in question if key in self.services[name]["resource"][type]: value = self.services[name]["resource"][type][key] break if not value: # Device isn't actually registered at present return RES_SUCCESS else: self.services[service_name][namespace][type][key] = value # Don't pass non-registration exceptions to clients try: if namespace == "resource": self._update_mdns(type) except Exception as e: self.logger.writeError("Exception registering with mDNS: {}".format(e)) try: self.aggregator.register_into(namespace, type, key, **self.preprocess_resource(type, key, value, NODE_REGVERSION)) self.logger.writeDebug("registering {} {}".format(type, key)) except Exception as e: self.logger.writeError("Exception registering {}: {}".format(namespace, e)) return RES_OTHERERROR return RES_SUCCESS def update_resource(self, service_name, pid, type, key, value): return self.register_resource(service_name, pid, type, key, value) def find_service(self, type, key): for service_name in self.services.keys(): if key in self.services[service_name]["resource"][type]: return service_name return None def unregister_resource(self, service_name, pid, type, key): if type not in self.permitted_resources: return RES_UNSUPPORTED return self._unregister(service_name, "resource", pid, type, key) def unregister_control(self, service_name, pid, device_id, control_data): # Note use of register here, as we're updating an existing Device return self._register(service_name, "control", pid, device_id, "remove", control_data) def _unregister(self, service_name, namespace, pid, type, key): if service_name not in self.services: return RES_NOEXISTS if self.services[service_name]["pid"] != pid: return RES_UNAUTHORISED if key == "00000000-0000-0000-0000-000000000000": return RES_OTHERERROR self.services[service_name][namespace][type].pop(key, None) # Don't pass non-registration exceptions to clients try: self.aggregator.unregister_from(namespace, type, key) except Exception as e: self.logger.writeError("Exception unregistering {}: {}".format(namespace, e)) return RES_OTHERERROR try: if namespace == "resource": self._update_mdns(type) except ServiceAlreadyExistsException as e: # We can't do anything about this, so just return success self.logger.writeError("Exception unregistering from mDNS: {}".format(e)) except Exception as e: self.logger.writeError("Exception unregistering from mDNS: {}".format(e)) return RES_OTHERERROR return RES_SUCCESS def list_services(self, api_version="v1.0"): return list(self.services.keys()) def get_service_href(self, name, api_version="v1.0"): if name not in self.services: return RES_NOEXISTS href = self.services[name]["href"] if self.services[name]["proxy_path"]: href += "/" + self.services[name]["proxy_path"] return href def get_service_type(self, name, api_version="v1.0"): if name not in self.services: return RES_NOEXISTS return self.services[name]["type"] def preprocess_url(self, url): parsed_url = urlparse(url) scheme = parsed_url.scheme if PROTOCOL == "https": if scheme == "http": scheme = "https" elif scheme == "ws": scheme = "wss" netloc = self.node_data["host"] if parsed_url.port: netloc += ":{}".format(parsed_url.port) parsed_url = parsed_url._replace(netloc=netloc, scheme=scheme) return urlunparse(parsed_url) def preprocess_resource(self, type, key, value, api_version="v1.0"): if type == "device": value_copy = copy.deepcopy(value) for name in self.services: if key in self.services[name]["control"] and "controls" in value_copy: value_copy["controls"] = value_copy["controls"] + list(self.services[name]["control"][key].values()) if "controls" in value_copy: for control in value_copy["controls"]: control["href"] = self.preprocess_url(control["href"]) return translate_api_version(value_copy, type, api_version) elif type == "sender": value_copy = copy.deepcopy(value) if "manifest_href" in value_copy: value_copy["manifest_href"] = self.preprocess_url(value_copy["manifest_href"]) return translate_api_version(value_copy, type, api_version) else: return translate_api_version(value, type, api_version) def list_resource(self, type, api_version="v1.0"): if type not in self.permitted_resources: return RES_UNSUPPORTED response = {} for name in self.services: response = (dict(list(response.items()) + [ (k, self.preprocess_resource(type, k, x, api_version)) for (k, x) in self.services[name]["resource"][type].items() if (api_version == "v1.0" or ( "max_api_version" in x and api_ver_compare(x["max_api_version"], api_version) >= 0 )) ])) return response def _len_resource(self, type): response = 0 for name in self.services: response += len(self.services[name]["resource"][type]) return response def _update_mdns(self, type): if type not in self.permitted_resources: return RES_UNSUPPORTED if not self.mdns_updater: return num_items = self._len_resource(type) if num_items == 1: try: self.mdns_updater.update_mdns(type, "register") except Exception: self.mdns_updater.update_mdns(type, "update") elif num_items == 0: self.mdns_updater.update_mdns(type, "unregister") else: self.mdns_updater.update_mdns(type, "update") def list_self(self, api_version="v1.0"): return self.preprocess_resource("node", self.node_data["id"], self.node_data, api_version) def _ptp_clock(self): clk = { "name": "clk1", "ref_type": "ptp", "version": "IEEE1588-2008", "traceable": False, "gmid": "00-00-00-00-00-00-00-00", "locked": False, } sts = IppClock().PTPStatus() if len(sts.keys()) > 0: clk['traceable'] = sts['timeTraceable'] clk['gmid'] = sts['grandmasterClockIdentity'].lower() clk['locked'] = (sts['ofm'][0] == 0) return clk def update_ptp(self): if IPP_UTILS_CLOCK_AVAILABLE: old_clk = None if "clk1" in self.clocks: old_clk = copy.copy(self.clocks["clk1"]) clk = self._ptp_clock() if old_clk is None: self.register_clock(clk) elif clk != old_clk: self.update_clock(clk) def register_clock(self, clk_data): if "name" not in clk_data: return RES_OTHERERROR if clk_data["name"] in self.clocks: return RES_EXISTS self.clocks[clk_data["name"]] = clk_data self.update_node() return RES_SUCCESS def update_clock(self, clk_data): if "name" not in clk_data: return RES_OTHERERROR if clk_data["name"] in self.clocks: self.clocks[clk_data["name"]] = clk_data self.update_node() return RES_SUCCESS return RES_NOEXISTS def unregister_clock(self, clk_name): if clk_name in self.clocks: del self.clocks[clk_name] self.update_node() return RES_SUCCESS return RES_NOEXISTS
class Aggregator(object): """This class serves as a proxy for the distant aggregation service running elsewhere on the network. It will search out aggregators and locate them, falling back to other ones if the one it is connected to disappears, and resending data as needed.""" def __init__(self, logger=None, mdns_updater=None, auth_registry=None): self.logger = Logger("aggregator_proxy", logger) self.mdnsbridge = IppmDNSBridge(logger=self.logger) self.aggregator_apiversion = None self.service_type = None self._set_api_version_and_srv_type( _config.get('nodefacade').get('NODE_REGVERSION')) self.aggregator = None self.registration_order = [ "device", "source", "flow", "sender", "receiver" ] self._mdns_updater = mdns_updater # '_node_data' is a local mirror of aggregated items. self._node_data = { 'node': None, 'registered': False, 'entities': { 'resource': {} } } self._running = True self._aggregator_list_stale = True self._aggregator_failure = False # Variable to flag when aggregator has returned and unexpected error self._backoff_active = False self._backoff_period = 0 self.auth_registrar = None # Class responsible for registering with Auth Server self.auth_registry = auth_registry # Top level class that tracks locally registered OAuth clients self.auth_client = None # Instance of Oauth client responsible for performing token requests self._reg_queue = gevent.queue.Queue() self.main_thread = gevent.spawn(self._main_thread) self.queue_thread = gevent.spawn(self._process_queue) def _set_api_version_and_srv_type(self, api_ver): """Set the aggregator api version equal to parameter and DNS-SD service type based on api version""" self.aggregator_apiversion = api_ver self._set_service_type(api_ver) def _set_service_type(self, api_ver): """Set DNS-SD service type based on current api version in use""" if api_ver in ['v1.0', 'v1.1', 'v1.2']: self.service_type = LEGACY_REG_MDNSTYPE else: self.service_type = REGISTRATION_MDNSTYPE def _main_thread(self): """The main thread runs in the background. If, when it runs, the Node is believed to be registered it will perform a heartbeat every 5 seconds. If the Node is not registered it will try to register the Node""" self.logger.writeDebug("Starting main thread") while self._running: if self._node_data["node"] and self.aggregator is None: self._discovery_operation() elif self._node_data["node"] and self._node_data["registered"]: self._registered_operation() else: self._node_data["registered"] = False self.aggregator = None gevent.sleep(0.2) self.logger.writeDebug("Stopping heartbeat thread") def _discovery_operation(self): """In Discovery operation the Node will wait a backoff period if defined to allow aggregators to recover when in a state of error. Selecting the most appropriate aggregator and try to register with it. If a registration fails then another aggregator will be tried.""" self.logger.writeDebug("Entering Discovery Mode") # Wait backoff period # Do not wait backoff period if aggregator failed, a new aggregator should be tried immediately if not self._aggregator_failure: self._back_off_timer() self._aggregator_failure = False # Update cached list of aggregators if self._aggregator_list_stale: self._flush_cached_aggregators() while True: self.aggregator = self._get_aggregator() if self.aggregator is None: self.logger.writeDebug("Failed to find registration API") break self.logger.writeDebug("Aggregator set to: {}".format( self.aggregator)) # Perform initial heartbeat, which will attempt to register Node if not already registered if self._heartbeat(): # Successfully registered Node with aggregator andproceed to registered operation # Else will try next aggregator break def _registered_operation(self): """In Registered operation, the Node is registered so a heartbeat will be performed, if the heartbeat is successful the Node will wait 5 seconds before attempting another heartbeat. Else another aggregator will be selected""" if not self._heartbeat(): # Heartbeat failed # Flag to update cached list of aggregators and immediately try new aggregator self.aggregator = None self._aggregator_failure = True def _heartbeat(self): """Performs a heartbeat to registered aggregator If heartbeat fails it will take actions to correct the error, by re-registering the Node If successfull will return True, else will return False""" if not self.aggregator: return False try: R = self._send( "POST", self.aggregator, self.aggregator_apiversion, "health/nodes/{}".format( self._node_data["node"]["data"]["id"])) if R.status_code == 200 and self._node_data["registered"]: # Continue to registered operation self.logger.writeDebug( "Successful heartbeat for Node {}".format( self._node_data["node"]["data"]["id"])) self._registered() heartbeat_wait = 5 while heartbeat_wait > 0 and self._running: gevent.sleep(1) heartbeat_wait -= 1 return True elif R.status_code in [200, 409]: # Delete node from registry if self._unregister_node(R.headers.get('Location')): return self._register_node(self._node_data["node"]) else: # Try next registry return False except InvalidRequest as e: if e.status_code == 404: # Re-register self.logger.writeWarning( "404 error on heartbeat. Marking Node for re-registration") self._node_data["registered"] = False return self._register_node(self._node_data["node"]) else: # Other error, try next registry return False except ServerSideError: self.logger.writeWarning( "Server Side Error on heartbeat. Trying another registry") return False except Exception as e: # Re-register self.logger.writeWarning( "Unexpected error on heartbeat: {}. Marking Node for re-registration" .format(e)) self._node_data["registered"] = False return False def _register_auth(self, client_name, client_uri): """Register OAuth client with Authorization Server""" self.logger.writeInfo( "Attempting to register dynamically with Auth Server") auth_registrar = AuthRegistrar(client_name=client_name, redirect_uri=PROTOCOL + '://' + FQDN + NODE_APIROOT + 'authorize', client_uri=client_uri, allowed_scope=ALLOWED_SCOPE, allowed_grant=ALLOWED_GRANTS) if auth_registrar.registered is True: return auth_registrar else: self.logger.writeWarning( "Unable to successfully register with Authorization Server") def _register_node(self, node_obj): """Attempt to register Node with aggregator Returns True is node was successfully registered with aggregator Returns False if registration failed If registration failed with 200 or 409, will attempt to delete and re-register""" if node_obj is None: return False # Drain the queue while not self._reg_queue.empty(): try: self._reg_queue.get(block=False) except gevent.queue.Queue.Empty: break try: # Try register the Node 3 times with aggregator before failing back to next aggregator for i in range(0, 3): R = self._send("POST", self.aggregator, self.aggregator_apiversion, "resource", node_obj) if R.status_code == 201: # Continue to registered operation self.logger.writeInfo( "Node Registered with {} at version {}".format( self.aggregator, self.aggregator_apiversion)) self._registered() # Trigger registration of Nodes resources self._register_node_resources() return True elif R.status_code in [200, 409]: # Delete node from aggregator & re-register if self._unregister_node(R.headers.get('Location')): continue else: # Try next aggregator return False except Exception as e: self.logger.writeError("Failed to register node {}".format(e)) return False def _register_node_resources(self): # Re-register items that must be ordered # Re-register things we have in the local cache. # "namespace" is e.g. "resource" # "entities" are the things associated under that namespace. for res_type in self.registration_order: for namespace, entities in self._node_data["entities"].items(): if res_type in entities: self.logger.writeInfo( "Ordered re-registration for type: '{}' in namespace '{}'" .format(res_type, namespace)) for key in entities[res_type]: self._queue_request("POST", namespace, res_type, key) # Re-register everything else # Re-register things we have in the local cache. # "namespace" is e.g. "resource" # "entities" are the things associated under that namespace. for namespace, entities in self._node_data["entities"].items(): for res_type in entities: if res_type not in self.registration_order: self.logger.writeInfo( "Unordered re-registration for type: '{}' in namespace '{}'" .format(res_type, namespace)) for key in entities[res_type]: self._queue_request("POST", namespace, res_type, key) def _registered(self): """Mark Node as registered and reset counters""" if (self._mdns_updater is not None): self._mdns_updater.P2P_disable() self._node_data['registered'] = True self._aggregator_list_stale = True self._reset_backoff_period() def _reset_backoff_period(self): self.logger.writeDebug("Resetting backoff period") self._backoff_period = 0 def _increase_backoff_period(self): """Exponentially increase the backoff period, until set maximum reached""" self.logger.writeDebug("Increasing backoff period") self._aggregator_list_stale = True if self._backoff_period == 0: self._backoff_period = BACKOFF_INITIAL_TIMOUT_SECONDS return self._backoff_period *= 2 if self._backoff_period > BACKOFF_MAX_TIMEOUT_SECONDS: self._backoff_period = BACKOFF_MAX_TIMEOUT_SECONDS def _back_off_timer(self): """Sleep for defined backoff period""" self.logger.writeDebug("Backoff timer enabled for {} seconds".format( self._backoff_period)) self._backoff_active = True gevent.sleep(self._backoff_period) self._backoff_active = False def _flush_cached_aggregators(self): """Flush the list of cached aggregators in the mdns bridge client, preventing the use of out of date aggregators""" self.logger.writeDebug("Flushing cached list of aggregators") self._aggregator_list_stale = False self.mdnsbridge.updateServices(self.service_type) def _get_aggregator(self): """Get the most appropriate aggregator from the mdns bridge client. If no aggregator found increment P2P counter, update cache and increase backoff If reached the end of available aggregators update cache and increase backoff""" try: return self.mdnsbridge.getHrefWithException( self.service_type, None, self.aggregator_apiversion, PROTOCOL, OAUTH_MODE) except NoService: self.logger.writeDebug( "No Registration services found: {} {} {}".format( self.service_type, self.aggregator_apiversion, PROTOCOL)) if self._mdns_updater is not None: self._mdns_updater.inc_P2P_enable_count() self._increase_backoff_period() return None except EndOfServiceList: self.logger.writeDebug( "End of Registration services list: {} {} {}".format( self.service_type, self.aggregator_apiversion, PROTOCOL)) self._increase_backoff_period() return None def _unregister_node(self, url_path=None): """Delete node from registry, using url_path if specified""" if self.aggregator is None: self.logger.writeWarning( 'Could not un-register as no aggregator set') return False try: self._node_data['registered'] = False if url_path is None: R = self._send( 'DELETE', self.aggregator, self.aggregator_apiversion, 'resource/nodes/{}'.format( self._node_data['node']["data"]["id"])) else: parsed_url = urlparse(url_path) R = self._send_request('DELETE', self.aggregator, parsed_url.path) if R.status_code == 204: # Successfully deleted node from Registry self.logger.writeInfo( "Node unregistered from {} at version {}".format( self.aggregator, self.aggregator_apiversion)) return True else: return False except Exception as e: self.logger.writeDebug( 'Exception raised while un-registering {}'.format(e)) return False def _process_queue(self): """Provided the Node is believed to be correctly registered, hand off a single request to the SEND method On client error, clear the resource from the local mirror On other error, mark Node as unregistered and trigger re-registration""" self.logger.writeDebug("Starting HTTP queue processing thread") # Checks queue not empty before quitting to make sure unregister node gets done while self._running: if (not self._node_data["registered"] or self._reg_queue.empty() or self._backoff_active or not self.aggregator): gevent.sleep(1) else: try: queue_item = self._reg_queue.get() namespace = queue_item["namespace"] res_type = queue_item["res_type"] res_key = queue_item["key"] if queue_item["method"] == "POST": if res_type == "node": send_obj = self._node_data.get("node") else: send_obj = self._node_data["entities"][namespace][ res_type].get(res_key) if send_obj is None: self.logger.writeError( "No data to send for resource {}".format( res_type)) continue try: self._send("POST", self.aggregator, self.aggregator_apiversion, "{}".format(namespace), send_obj) self.logger.writeInfo("Registered {} {} {}".format( namespace, res_type, res_key)) except InvalidRequest as e: self.logger.writeWarning( "Error registering {} {}: {}".format( res_type, res_key, e)) self.logger.writeWarning( "Request data: {}".format(send_obj)) del self._node_data["entities"][namespace][ res_type][res_key] elif queue_item["method"] == "DELETE": translated_type = res_type + 's' if namespace == "resource" and res_type == "node": # Handle special Node type self._node_data["node"] = None self._node_data["registered"] = False try: self._send( "DELETE", self.aggregator, self.aggregator_apiversion, "{}/{}/{}".format(namespace, translated_type, res_key)) self.logger.writeInfo( "Un-registered {} {} {}".format( namespace, translated_type, res_key)) except InvalidRequest as e: self.logger.writeWarning( "Error deleting resource {} {}: {}".format( translated_type, res_key, e)) else: self.logger.writeWarning( "Method {} not supported for Registration API interactions" .format(queue_item["method"])) except ServerSideError: self.aggregator = None self._aggregator_failure = True self._add_request_to_front_of_queue(queue_item) except Exception as e: self.logger.writeError( "Unexpected Error while processing queue, marking Node for re-registration\n" "{}".format(e)) self._node_data["registered"] = False self.aggregator = None if (self._mdns_updater is not None): self._mdns_updater.P2P_disable() self.logger.writeDebug("Stopping HTTP queue processing thread") def _queue_request(self, method, namespace, res_type, key): """Queue a request to be processed. Handles all requests except initial Node POST which is done in _process_reregister""" self._reg_queue.put({ "method": method, "namespace": namespace, "res_type": res_type, "key": key }) def _add_request_to_front_of_queue(self, request): """Adds item to the front of the queue""" new_queue = deque() new_queue.append(request) # Drain the queue while not self._reg_queue.empty(): try: new_queue.append(self._reg_queue.get()) except gevent.queue.Queue.Empty: break # Add items back to the queue while True: try: self._reg_queue.put(new_queue.popleft()) except IndexError: break def register_auth_client(self, client_name, client_uri): """Function for Registering OAuth client with Auth Server and instantiating OAuth Client class""" if OAUTH_MODE is True: if self.auth_registrar is None: self.auth_registrar = self._register_auth( client_name=client_name, client_uri=client_uri) if self.auth_registrar and self.auth_client is None: try: # Register Node Client self.auth_registry.register_client( client_name=client_name, client_uri=client_uri, **self.auth_registrar.server_metadata) self.logger.writeInfo( "Successfully registered Auth Client") except (OSError, IOError): self.logger.writeError( "Exception accessing OAuth credentials. This may be a file permissions issue." ) return # Extract the 'RemoteApp' class created when registering self.auth_client = getattr(self.auth_registry, client_name) # Fetch Token self.get_auth_token() def get_auth_token(self): """Fetch Access Token either using redirection grant flow or using auth_client""" if self.auth_client is not None and self.auth_registrar is not None: try: if "authorization_code" in self.auth_registrar.client_metadata.get( "grant_types", {}): self.logger.writeInfo( "Endpoint '/oauth' on Node API will provide redirect to authorization endpoint on Auth Server." ) return elif "client_credentials" in self.auth_registrar.client_metadata.get( "grant_types", {}): # Fetch Token token = self.auth_client.fetch_access_token() # Store token in member variable to be extracted using `fetch_local_token` function self.auth_registry.bearer_token = token else: raise OAuth2Error( "Client not registered with supported Grant Type") except OAuth2Error as e: self.logger.writeError( "Failure fetching access token. {}".format(e)) def register(self, res_type, key, **kwargs): """Register 'resource' type data including the Node NB: Node registration is managed by heartbeat thread so may take up to 5 seconds! """ self.register_into("resource", res_type, key, **kwargs) def unregister(self, res_type, key): """Unregister 'resource' type data including the Node""" self.unregister_from("resource", res_type, key) def register_into(self, namespace, res_type, key, **kwargs): """General register method for 'resource' types""" data = kwargs send_obj = {"type": res_type, "data": data} if 'id' not in send_obj["data"]: self.logger.writeWarning( "No 'id' present in data, using key='{}': {}".format( key, data)) send_obj["data"]["id"] = key if namespace == "resource" and res_type == "node": # Ensure Registered with Auth Server (is there a better place for this) if OAUTH_MODE is True: self.register_auth_client("nmos-node-{}".format(data["id"]), FQDN) # Handle special Node type when Node is not registered, by immediately registering if self._node_data["node"] is None: # Will trigger registration in main thread self._node_data["node"] = send_obj return # Update Node Data self._node_data["node"] = send_obj else: self._add_mirror_keys(namespace, res_type) self._node_data["entities"][namespace][res_type][key] = send_obj self._queue_request("POST", namespace, res_type, key) def unregister_from(self, namespace, res_type, key): """General unregister method for 'resource' types""" if namespace == "resource" and res_type == "node": # Handle special Node type self._unregister_node() self._node_data["node"] = None return elif res_type in self._node_data["entities"][namespace]: self._add_mirror_keys(namespace, res_type) if key in self._node_data["entities"][namespace][res_type]: del self._node_data["entities"][namespace][res_type][key] self._queue_request("DELETE", namespace, res_type, key) def _add_mirror_keys(self, namespace, res_type): """Deal with missing keys in local mirror""" if namespace not in self._node_data["entities"]: self._node_data["entities"][namespace] = {} if res_type not in self._node_data["entities"][namespace]: self._node_data["entities"][namespace][res_type] = {} def stop(self): """Stop the Aggregator object running""" self.logger.writeDebug("Stopping aggregator proxy") self._running = False self.main_thread.join() self.queue_thread.join() def status(self): """Return the current status of node in the aggregator""" return { "api_href": self.aggregator, "api_version": self.aggregator_apiversion, "registered": self._node_data["registered"] } def _send(self, method, aggregator, api_ver, url, data=None): """Handle sending request to the registration API, with error handling HTTP 200, 201, 204, 409 - Success, return response Timeout, HTTP 5xx, Connection Error - Raise ServerSideError Exception HTTP 4xx - Raise InvalidRequest Exception""" url = "{}/{}/{}".format(AGGREGATOR_APIROOT, api_ver, url) try: resp = self._send_request(method, aggregator, url, data) if resp is None: self.logger.writeWarning( "No response from aggregator {}".format(aggregator)) raise ServerSideError elif resp.status_code in [200, 201, 204, 409]: return resp elif (resp.status_code // 100) == 4: self.logger.writeWarning( "{} response from aggregator: {} {}".format( resp.status_code, method, urljoin(aggregator, url))) self.logger.writeDebug("\nResponse: {}".format(resp.content)) raise InvalidRequest(resp.status_code) else: self.logger.writeWarning( "Unexpected status from aggregator {}: {}, {}".format( aggregator, resp.status_code, resp.content)) raise ServerSideError except requests.exceptions.RequestException as e: # Log a warning, then let another aggregator be chosen self.logger.writeWarning("{} from aggregator {}".format( e, aggregator)) raise ServerSideError def _send_request(self, method, aggregator, url_path, data=None): """Low level method to send a HTTP request""" url = urljoin(aggregator, url_path) self.logger.writeDebug("{} {}".format(method, url)) # We give a long(ish) timeout below, as the async request may succeed after the timeout period # has expired, causing the node to be registered twice (potentially at different aggregators). # Whilst this isn't a problem in practice, it may cause excessive churn in websocket traffic # to web clients - so, sacrifice a little timeliness for things working as designed the # majority of the time... kwargs = {"method": method, "url": url, "json": data, "timeout": 1.0} if _config.get('prefer_ipv6') is True: kwargs["proxies"] = {'http': ''} # If not in OAuth mode, perform standard request if OAUTH_MODE is False or self.auth_client is None: return requests.request(**kwargs) else: # If in OAuth Mode, use OAuth client to automatically fetch token / refresh token if expired with self.auth_registry.app.app_context(): try: return self.auth_client.request(**kwargs) # General OAuth Error (e.g. incorrect request details, invalid client, etc.) except OAuth2Error as e: self.logger.writeError( "Failed to fetch token before making API call to {}. {}" .format(url, e)) self.auth_registrar = self.auth_client = None
class Facade(object): """This class serves as a proxy for the Facade running on the same machine if it exists. If no facade exists on this machine then it will do nothing, but calls will still function without throwing any exceptions.""" def __init__(self, srv_type, address="ipc:///tmp/ips-nodefacade", logger=None): self.logger = Logger("facade_proxy", logger) self.ipc = None self.srv_registered = False # Flag whether service is registered self.reregister = False # Flag whether resources are correctly registered self.address = address self.srv_type = srv_type.lower() self.srv_type_urn = "urn:x-ipstudio:service:" + self.srv_type self.pid = os.getpid() self.resources = {} self.controls = {} self.href = None self.proxy_path = None self.lock = Lock() # Protect access to IPC socket def setup_ipc(self): with self.lock: try: self.ipc = Proxy(self.address) except Exception: self.ipc = None def register_service(self, href, proxy_path): self.logger.writeInfo("Register service") self.href = href self.proxy_path = proxy_path if not self.ipc: self.setup_ipc() if not self.ipc: return try: with self.lock: s = self.ipc.srv_register(self.srv_type, self.srv_type_urn, self.pid, href, proxy_path) if s == FAC_SUCCESS: self.srv_registered = True else: self.logger.writeInfo("Service registration failed: {}".format(self.debug_message(s))) except Exception as e: self.logger.writeError("Exception when registering service: {}".format(str(e))) self.ipc = None def unregister_service(self): if not self.ipc: self.setup_ipc() if not self.ipc: return try: with self.lock: self.ipc.srv_unregister(self.srv_type, self.pid) self.srv_registered = False except Exception as e: self.logger.writeError("Exception when unregistering service: {}".format(str(e))) self.ipc = None def heartbeat_service(self): if not self.ipc: self.setup_ipc() if not self.ipc: return try: with self.lock: s = self.ipc.srv_heartbeat(self.srv_type, self.pid) if s != FAC_SUCCESS: self.srv_registered = False self.logger.writeInfo("Heartbeat failed: {}".format(self.debug_message(s))) else: self.srv_registered = True if not self.srv_registered or self.reregister: # Handle reconnection if facade disappears self.logger.writeInfo("Reregistering all services") self.reregister_all() except Exception as e: self.logger.writeError("Exception when heartbeating service: {}".format(str(e))) self.ipc = None # ONLY call this directly from within heartbeat_service! # To cause a re-registration on failure, set self.reregister! def reregister_all(self): self.unregister_service() if self.srv_registered: return self.register_service(self.href, self.proxy_path) if not self.srv_registered: return # TODO(clyntp): the following blocks are so similar... # re-register resources for type in self.resources: for key in self.resources[type]: try: with self.lock: resource = self.resources[type][key] # Hide some implementation details for receivers if type == "receiver": resource = deepcopy(self.resources[type][key]) if "pipel_id" in self.resources[type][key]: resource.pop('pipel_id') if "pipeline_id" in self.resources[type][key]: resource.pop('pipeline_id') self.ipc.res_register(self.srv_type, self.pid, type, key, resource) except Exception as e: self.logger.writeError("Exception when re-registering resource: {}".format(str(e))) self.ipc = None gevent.sleep(0) return # re-register controls for device_id in self.controls: for control_href in self.controls[device_id]: control_data = self.controls[device_id][control_href] try: with self.lock: self.ipc.control_register(self.srv_type, self.pid, device_id, control_data) except Exception as e: self.logger.writeError("Exception when re-registering control: {}".format(str(e))) self.ipc = None gevent.sleep(0) return self.reregister = False def _call_ipc_method(self, method, *args, **kwargs): if not self.srv_registered: # Don't attempt if not registered - will just hit many timeouts self.reregister = True return if not self.ipc: self.setup_ipc() if not self.ipc: self.reregister = True return try: with self.lock: return self.ipc.invoke_named(method, self.srv_type, self.pid, *args, **kwargs) except Exception as e: self.logger.writeError("Exception when calling IPC method: {}".format(str(e))) self.ipc = None self.reregister = True def addResource(self, type, key, value): value = deepcopy(value) if type not in self.resources: self.resources[type] = {} self.resources[type][key] = value self._call_ipc_method("res_register", type, key, value) def updateResource(self, type, key, value): value = deepcopy(value) if type not in self.resources: self.resources[type] = {} self.resources[type][key] = value self._call_ipc_method("res_update", type, key, value) def delResource(self, type, key): if type in self.resources: # Hack until adoption of flow instances (Ensure transports for flow are deleted) if type == "flow" and "transport" in self.resources: for transport in tuple(self.resources["transport"].keys()): if key == self.resources["transport"][transport]["flow-id"]: del self.resources["transport"][transport] if key in self.resources[type]: del self.resources[type][key] self._call_ipc_method("res_unregister", type, key) def addControl(self, device_id, control_data): if device_id not in self.controls: self.controls[device_id] = {} self.controls[device_id][control_data["href"]] = control_data self._call_ipc_method("control_register", device_id, control_data) def delControl(self, device_id, control_data): if device_id in self.controls: self.controls[device_id].pop(control_data["href"], None) self._call_ipc_method("control_unregister", device_id, control_data) def get_node_self(self, api_version="v1.1"): return self._call_ipc_method("self_get", api_version) def get_reg_status(self): return self._call_ipc_method("status_get") def addClock(self, clk_data): self._call_ipc_method("clock_register", clk_data) def updateClock(self, clk_data): self._call_ipc_method("clock_update", clk_data) def delClock(self, clk_name): self._call_ipc_method("clock_unregister", clk_name) def debug_message(self, code): msg = {FAC_SUCCESS: "Success!", FAC_EXISTS: "Service already exists", FAC_UNREGISTERED: "Service isn't yet registered", FAC_UNAUTHORISED: "Unauthorised", FAC_UNSUPPORTED: "Unsupported", FAC_OTHERERROR: "Other error"}[code] return msg
class QueryService(object): def __init__(self, mdns_bridge, logger=None, apiversion=QUERY_APIVERSION, priority=None): self.mdns_bridge = mdns_bridge self._query_url = self.mdns_bridge.getHref(QUERY_MDNSTYPE, priority) iter = 0 #TODO FIXME: Remove once IPv6 work complete and Python can use link local v6 correctly while "fe80:" in self._query_url: self._query_url = self.mdns_bridge.getHref(QUERY_MDNSTYPE, priority) iter += 1 if iter > 20: break self.logger = Logger("nmoscommon.query", logger) self.apiversion = apiversion self.priority = priority def _get_query(self, url): backoff = [0.3, 0.7, 1.0] for try_i in xrange(len(backoff)): try: response = requests.get("{}/{}/{}/{}{}".format( self._query_url, QUERY_APINAMESPACE, QUERY_APINAME, self.apiversion, url)) response.raise_for_status() return response except Exception as e: self.logger.writeWarning( "Could not GET from query service at {}{}: {}".format( self._query_url, url, e)) if try_i == len(backoff) - 1: raise QueryNotFoundError(e) # TODO: sleep between requests to back off self._query_url = self.mdns_bridge.getHref( QUERY_MDNSTYPE, self.priority) self.logger.writeInfo("Trying query at: {}".format( self._query_url)) # Shouldn't get this far, but don't return None raise QueryNotFoundError( "Could not find a query service (should be unreachable!)" ) # pragma: no cover def get_services(self, service_urn, node_id=None): """ Look for nodes which contain a particular service type. Returns a list of found service objects, or an empty list on not-found. May raise a QueryNotFound exception if query service can't be contacted. """ response = self._get_query("/nodes/") if response.status_code != 200: self.logger.writeError( "Could not get /nodes/ from query service at {}".format( self._query_url)) return [] nodes = response.json() services = [] if node_id == None: services = itertools.chain.from_iterable( [n.get('services', []) for n in nodes]) else: services = itertools.chain.from_iterable( [n.get('services', []) for n in nodes if n["id"] == node_id]) return [s for s in services if s.get('type', 'unknown') == service_urn] def subscribe_topic(self, topic, on_event, on_open=None): """ Subscribe to a query service topic, calling `on_event` for changes. Will block unless wrapped in a gevent greenlet: gevent.spawn(qs.subscribe_topic, "flows", on_event) If `on_open` is given, it will be called when the websocket is opened. """ query_url = self.mdns_bridge.getHref(QUERY_MDNSTYPE, self.priority) if query_url == "": raise BadSubscriptionError( "Could not get query service from mDNS bridge") query_url = query_url + "/" + QUERY_APINAMESPACE + "/" + QUERY_APINAME + "/" + self.apiversion resource_path = "/" + topic.strip("/") params = { "max_update_rate_ms": 100, "persist": False, "resource_path": resource_path, "params": {} } r = requests.post(query_url + "/subscriptions", data=json.dumps(params), proxies={'http': ''}) if r.status_code not in [200, 201]: raise BadSubscriptionError("{}: {}".format(r.status_code, r.text)) r_json = r.json() if not "ws_href" in r_json: raise BadSubscriptionError( "Result has no 'ws_href': {}".format(r_json)) assert (query_url.startswith("http://")) ws_href = r_json.get("ws_href") # handlers for websocket events def _on_open(*args): if on_open is not None: on_open() def _on_close(*args): pass def _on_message(*args): assert (len(args) >= 1) data = json.loads(args[1]) events = data["grain"]["data"] if isinstance(events, dict): events = [events] for event in events: on_event(event) # Open websocket connection, and poll sock = websocket.WebSocketApp(ws_href, on_open=_on_open, on_message=_on_message, on_close=_on_close) if sock is None: raise BadSubscriptionError( "Could not open websocket at {}".format(ws_href)) sock.run_forever()
class QueryCommon(object): def __init__(self, logger=None, api_version="v1.0"): self.logger = Logger("regquery", _parent=logger) self.query_sockets = QuerySocketsCommon(WS_PORT, logger=self.logger) # there is a choice here: watch at specific top levels (if flat), or watch all data. # initially, watch all data - this may be less than ideal. self.watcher = ChangeWatcher(reg['host'], reg['port'], handler=self, logger=self.logger) self.watcher.start() self.api_version = api_version def _cleanup(self): self.watcher.stop() self.watcher.join(timeout=5) # generates a predictable UID for this process def gen_source_id(self): seed = '{}{}'.format(os.getpid(), socket.gethostname()) uid = str(uuid.uuid3(uuid.NAMESPACE_DNS, seed)) return uid # parse services and render as a dictionary def parse_services_dict(self, obj, url, args, verbose): res_type_pattern = None if url is not None and url != '/' and url != '': res_type_pattern = util.translate_resourcetypes(url) if 'node' in obj: unpacked = etcd_unpack(obj) nodes = self._match_nodes(unpacked, res_type_pattern, args, verbose) return nodes return [] # extract objects of given types that also match supplied url and args def _match_nodes(self, obj, pattern, args, verbose): retval = [] for k, v in obj.items(): if any(rtype in k for rtype in VALID_TYPES) and type(v) is unicode: if self._matches_path(k, pattern): downgrade_ver = None if args and "query.downgrade" in args: downgrade_ver = args["query.downgrade"] # Downgrade / convert any mis-versioned objects as required resource_type = util.get_resourcetypes(k).replace("/", "") json_repr = None if resource_type != "": json_repr = json.loads(v) json_repr = version_transforms.convert( json_repr, resource_type, self.api_version, downgrade_ver) # If nothing could be downgraded, skip over the object if not json_repr: continue node = self._summarise(json_repr) if self._matches_args(node, args): if verbose: retval.append(node) else: retval.append(node['id']) elif type(v) is dict: # explore more retval = retval + self._match_nodes(v, pattern, args, verbose) return retval # see if href matches supplied regex def _matches_path(self, href, pattern): return pattern is None or pattern in href # see if object matches supplied arguments def _matches_args(self, obj, args): arg_checker = QueryFilterCommon() return arg_checker.check_args(args, obj) # summarise service in a presentable way def _summarise(self, json_repr): if not json_repr: return {} removals = (x for x in json_repr.keys() if x.startswith("@_")) for key in removals: del json_repr[key] return json_repr def _process_response(self, response): """ Process a response from a GET long-poll on etcd (watch). `response' is a dict, decoded from JSON. """ self.logger.writeDebug('process response {}'.format(response)) if response['action'] == 'set' or response['action'] == 'delete': unpacked = etcd_unpack(response) for k, v in unpacked.items(): restype = util.get_resourcetypes(k) if restype in VALID_TYPES: n_obj = {} pn_obj = {} if v: n_obj = json.loads(v.get('node', '{}')) pn_obj = json.loads(v.get('prevNode', '{}')) if response['action'] == 'set' and pn_obj != n_obj: self.do_sup(k, pn_obj, n_obj) elif response['action'] == 'delete': self.do_sdown(k, pn_obj, n_obj) else: self.logger.writeError( "Invalid type '{}' in response.".format(restype)) # Queries # get data for supplied path def get_data_for_path(self, path, args): # Set verbosity verbose = not string.lower(args.get('verbose', '')) == 'false' url = 'http://%s:%i/v2/keys/resource/?recursive=true' % (reg['host'], reg['port']) response = requests.request('GET', url, proxies={'http': ''}) if response.status_code != 200: self.logger.writeError('bad status_code %i' % response.status_code) return None else: return self.parse_services_dict(json.loads(response.text), path, args, verbose) def get_ws_subscribers(self, socket_id=None): obj = None if socket_id: obj = self.query_sockets.get_socket(socket_id) else: obj = self.query_sockets.get_socketlist() return obj def post_ws_subscribers(self, json): obj, created = self.query_sockets.post_socket(json) return obj, created def delete_ws_subscribers(self, socket_id): res = self.query_sockets.delete_socket(socket_id) return res def do_sync(self, ws, socket): # HTTP GET on etcd registry at top level path = util.translate_resourcetypes(socket.resource_path) url = 'http://{}:{}/v2/keys/resource/{}?recursive=true'.format( reg['host'], reg['port'], path) event = GrainEvent() event.source_id = self.gen_source_id() event.topic = socket.resource_path event.flow_id = socket.uuid # TODO: could get expensive with lots of flows... try: r = requests.request('GET', url, proxies={'http': ''}) if r.status_code not in [200, 404]: err = { "type": "error", "data": "{} getting resources of topic {}".format( r.status_code, path) } ws.send(json.dumps(err)) return err obj = json.loads(r.text) unpacked = etcd_unpack(obj) nodes = self._match_nodes(unpacked, path, socket.params, verbose=True) for node in nodes: event.addGrainFromObj(pre_obj=node, post_obj=node) ws.send(json.dumps(event.obj())) except Exception as err: self.logger.writeError('Exception in do_sync: {}'.format(err)) def do_sup(self, path, pn_obj, n_obj): self.logger.writeDebug('do_sup {}'.format(path)) if cmp(n_obj, pn_obj) == 0: return ws = self.query_sockets.find_socks(path=path, obj=n_obj, p_obj=pn_obj) event = GrainEvent() event.source_id = self.gen_source_id() event.topic = util.get_resourcetypes(path) for s in ws: self.logger.writeDebug('next ws ' + s.ws_href) downgrade_ver = None if s.params and "query.downgrade" in s.params: downgrade_ver = s.params["query.downgrade"] s_n_obj = version_transforms.convert(n_obj, event.topic.replace("/", ""), self.api_version, downgrade_ver) s_pn_obj = version_transforms.convert(pn_obj, event.topic.replace("/", ""), self.api_version, downgrade_ver) if not s_n_obj and not s_pn_obj: continue s_n_obj = self._summarise(s_n_obj) s_pn_obj = self._summarise(s_pn_obj) event.flow_id = s.uuid event.clearGrains() if not self._matches_args(s_pn_obj, s.params): # Didn't previously match filter, so should be returned event.addGrainFromObj(pre_obj={}, post_obj=n_obj) elif not self._matches_args(s_n_obj, s.params): # Doesn't match filter any longer, so shouldn't be returned event.addGrainFromObj(pre_obj=s_pn_obj, post_obj={}) else: event.addGrainFromObj(pre_obj=s_pn_obj, post_obj=s_n_obj) s.notify_subscribers(event.obj()) def do_sdown(self, path, pn_obj, n_obj): self.logger.writeDebug('do_sdown {}'.format(path)) ws = self.query_sockets.find_socks(path=path, obj=n_obj, p_obj=pn_obj) event = GrainEvent() event.source_id = self.gen_source_id() event.topic = util.get_resourcetypes(path) for s in ws: self.logger.writeDebug('next ws' + s.ws_href) downgrade_ver = None if s.params and "query.downgrade" in s.params: downgrade_ver = s.params["query.downgrade"] s_pn_obj = version_transforms.convert(pn_obj, event.topic.replace("/", ""), self.api_version, downgrade_ver) if not s_pn_obj: continue s_pn_obj = self._summarise(s_pn_obj) event.flow_id = s.uuid event.clearGrains() event.addGrainFromObj(pre_obj=s_pn_obj, post_obj={}) s.notify_subscribers(event.obj())
class EtcdEventQueue(object): """ Attempt to overcome the "missed etcd event" issue, which can be caused when processing of a return from a http long-poll takes too long, and an event is missed in etcd. This uses etcd's "waitIndex" functionality, which has the caveat that only the last 1000 events are stored. So, whilst this scheme should not miss events for fast updates, the case where 1000 updates occur within the space of a single event being processed will still be missed. This is unlikely, but still possible, so a "sentinel" message with action=index_skip will be sent to the output queue when this happens. To use this, the `queue' member of EtcdEventQueue is iterable: q = EtcdEventQueue() for message in q.queue: # process This uses http://www.gevent.org/gevent.queue.html as an underlying data structure, so can be consumed from multiple greenlets if necessary. """ def __init__(self, host, port, logger=None): self.queue = gevent.queue.Queue() self._base_url = "http://{}:{}/v2/keys/resource/".format(host, port) self._long_poll_url = self._base_url + "?recursive=true&wait=true" self._greenlet = gevent.spawn(self._wait_event, 0) self._alive = True self._logger = Logger("etcd_watch", logger) def _get_index(self, current_index): index = current_index try: response = requests.get(self._base_url, proxies={'http': ''}, timeout=1) if response is not None: if response.status_code == 200: index = _get_etcd_index(response, self._logger) self._logger.writeDebug("waitIndex now = {}".format(index)) # Always want to know if the index we were waiting on was greater # than current index, as this indicates something that needs further # investigation... if index < current_index: self._logger.writeWarning("Index decreased! {} -> {}".format(current_index, index)) elif response.status_code in [400, 404]: # '/resource' not found in etcd yet, back off for a second and set waitIndex to value of the x-etcd-index header index = int(response.headers.get('x-etcd-index', 0)) self._logger.writeInfo("{} not found, wait... waitIndex={}".format(self._base_url, index)) gevent.sleep(1) else: # response was None... self._logger.writeWarning("Could not GET {} after timeout; waitIndex now=0".format(self._base_url)) index = 0 except Exception as ex: # Getting the new index failed, so reset to 0. self._logger.writeWarning("Reset waitIndex to 0, error: {}".format(ex)) index = 0 return index def _wait_event(self, since): current_index = since while self._alive: req = None try: # Make the long-poll request to etcd using the current # "waitIndex". A timeout is used as situations have been # observed where the etcd modification index decreases (caused # by network partition or by a node having it's data reset?), # and the query service is not restarted, hence the code below # is left waiting for a much higher modification index than it # should. To mitigate this simply, when a timeout occurs, # assume that the modified index is "wrong", and forcibly try # to fetch the next index. This may "miss" updates, which is of # limited consequence. An enhancement (and therefore # complication...) could use the fact that the timeout is # small, and set waitIndex to the x-etcd-index result minus # some heuristically determined number of updates, to try and # catch the "back-in-time" updates stored in etcd's log, but # this feels brittle and overcomplicated for something that # could be solved by a browser refresh/handling of the "skip" # event to request a full set of resources. # https://github.com/coreos/etcd/blob/master/Documentation/api.md#waiting-for-a-change next_index_param = "&waitIndex={}".format(current_index + 1) req = requests.get(self._long_poll_url + next_index_param, proxies={'http': ''}, timeout=20) except socket.timeout: # Get a new wait index to watch from by querying /resource self._logger.writeDebug("Timeout waiting on long-poll. Refreshing waitIndex...") current_index = self._get_index(current_index) continue except Exception as ex: self._logger.writeWarning("Could not contact etcd: {}".format(ex)) gevent.sleep(5) continue if req is not None: # Decode payload, which should be json... try: json = req.json() except Exception: self._logger.writeError("Error decoding payload: {}".format(req.text)) continue if req.status_code == 200: # Return from request was OK, so put the payload on the queue. # NOTE: we use the "modifiedIndex" of the _node_ we receive, NOT the header. # This follows the etcd docs linked above. self.queue.put(json) current_index = json.get('node', {}).get('modifiedIndex', current_index) else: # Error codes documented here: # https://github.com/coreos/etcd/blob/master/Documentation/errorcode.md self._logger.writeInfo("error: http:{}, etcd:{}".format(req.status_code, json.get('errorCode', 0))) if json.get('errorCode', 0) == 401: # Index has been cleared. This may cause missed events, so send an (invented) sentinel message to queue. new_index = self._get_index(current_index) self._logger.writeWarning("etcd history not available; skipping {} -> {}".format(current_index, new_index)) self.queue.put({'action': 'index_skip', 'from': current_index, 'to': new_index}) current_index = new_index def stop(self): self._logger.writeInfo("Stopping service") print "stopping" self._alive = False self._greenlet.kill(timeout=5) self.queue.put(StopIteration)
class FacadeRegistry(object): def __init__(self, resources, aggregator, mdns_updater, node_id, node_data, logger=None): # `node_data` must be correctly structured self.permitted_resources = resources self.services = {} self.aggregator = aggregator self.mdns_updater = mdns_updater self.node_id = node_id assert ("interfaces" in node_data ) # Check data conforms to latest supported API version self.node_data = node_data self.logger = Logger("facade_registry", logger) def modify_node(self, **kwargs): for key in kwargs.keys(): if key in self.node_data: self.node_data[key] = kwargs[key] self.update_node() def update_node(self): self.node_data["services"] = [] for service_name in self.services: href = None if self.services[service_name]["href"]: if self.services[service_name]["proxy_path"]: href = "http://{}/{}".format( self.node_data["host"], self.services[service_name]["proxy_path"]) self.node_data["services"].append({ "href": href, "type": self.services[service_name]["type"] }) self.node_data["version"] = str(ptptime.ptp_detail()[0]) + ":" + str( ptptime.ptp_detail()[1]) try: self.aggregator.register( "node", self.node_id, **legalise_resource(self.node_data, "node", NODE_REGVERSION)) except Exception as e: self.logger.writeError( "Exception re-registering node: {}".format(e)) def register_service(self, name, srv_type, pid, href=None, proxy_path=None): if name in self.services: return RES_EXISTS self.services[name] = { "heartbeat": time.time(), "resource": {}, # Registered resources live under here "timeline": { "flowsegment": {} }, # Registered "timeline" items live under here "pid": pid, "href": href, "proxy_path": proxy_path, "type": srv_type } for resource_name in self.permitted_resources: self.services[name]["resource"][resource_name] = {} self.update_node() return RES_SUCCESS def update_service(self, name, pid, href=None, proxy_path=None): if not name in self.services: return RES_NOEXISTS if self.services[name]["pid"] != pid: return RES_UNAUTHORISED self.services[name]["heartbeat"] = time.time() self.services[name]["href"] = href self.services[name]["proxy_path"] = proxy_path self.update_node() return RES_SUCCESS def unregister_service(self, name, pid): if not name in self.services: return RES_NOEXISTS if self.services[name]["pid"] != pid: return RES_UNAUTHORISED for namespace in ["resource", "timeline"]: for type in self.services[name][namespace].keys(): for key in self.services[name][namespace][type].keys(): self._unregister(name, namespace, pid, type, key) self.services.pop(name, None) self.update_node() return RES_SUCCESS def heartbeat_service(self, name, pid): if not name in self.services: return RES_NOEXISTS if self.services[name]["pid"] != pid: return RES_UNAUTHORISED self.services[name]["heartbeat"] = time.time() return RES_SUCCESS def cleanup_services(self): timed_out = time.time() - HEARTBEAT_TIMEOUT for name in self.services.keys(): if self.services[name]["heartbeat"] < timed_out: self.unregister_service(name, self.services[name]["pid"]) def register_resource(self, service_name, pid, type, key, value): if not type in self.permitted_resources: return RES_UNSUPPORTED return self._register(service_name, "resource", pid, type, key, value) def register_to_timeline(self, service_name, pid, type, key, value): return self._register(service_name, "timeline", pid, type, key, value) def _register(self, service_name, namespace, pid, type, key, value): if "max_api_version" not in value: self.logger.writeWarning( "Service {}: Registration without valid api version specified". format(service_name)) value["max_api_version"] = "v1.0" elif api_version_less_than(value["max_api_version"], NODE_REGVERSION): self.logger.writeWarning( "Trying to register resource with api version too low: \"%s\" : %s", key, json.dumps(value)) if not service_name in self.services: return RES_NOEXISTS if not self.services[service_name]["pid"] == pid: return RES_UNAUTHORISED if key == "00000000-0000-0000-0000-000000000000": return RES_OTHERERROR # Add a node_id to those resources which need one if type == 'device': value['node_id'] = self.node_id self.services[service_name][namespace][type][key] = value # Don't pass non-registration exceptions to clients try: self._update_mdns(type) except Exception as e: self.logger.writeError( "Exception registering with mDNS: {}".format(e)) try: self.aggregator.register_into( namespace, type, key, **legalise_resource(value, type, NODE_REGVERSION)) self.logger.writeDebug("registering {} {}".format(type, key)) except Exception as e: self.logger.writeError("Exception registering {}: {}".format( namespace, e)) return RES_OTHERERROR return RES_SUCCESS def update_resource(self, service_name, pid, type, key, value): return self.register_resource(service_name, pid, type, key, value) def find_service(self, type, key): for service_name in self.services.keys(): if key in self.services[service_name]["resource"][type]: return service_name return None def update_timeline(self, service_name, pid, type, key, value): return self._register(service_name, "timeline", pid, type, key, value) def unregister_resource(self, service_name, pid, type, key): if not type in self.permitted_resources: return RES_UNSUPPORTED return self._unregister(service_name, "resource", pid, type, key) def unregister_from_timeline(self, service_name, pid, type, key): return self._unregister(service_name, "timeline", pid, type, key) def _unregister(self, service_name, namespace, pid, type, key): if not service_name in self.services: return RES_NOEXISTS if not self.services[service_name]["pid"] == pid: return RES_UNAUTHORISED if key == "00000000-0000-0000-0000-000000000000": return RES_OTHERERROR self.services[service_name][namespace][type].pop(key, None) # Don't pass non-registration exceptions to clients try: self.aggregator.unregister_from(namespace, type, key) except Exception as e: self.logger.writeError("Exception unregistering {}: {}".format( namespace, e)) return RES_OTHERERROR try: self._update_mdns(type) except Exception as e: extype, exmsg = e self.logger.writeError( "Exception unregistering from mDNS: {}".format(e)) if extype != -65548: # Name conflict return RES_OTHERERROR return RES_SUCCESS def list_services(self, api_version="v1.0"): return self.services.keys() def get_service_href(self, name, api_version="v1.0"): if not name in self.services: return RES_NOEXISTS href = self.services[name]["href"] if self.services[name]["proxy_path"]: href += "/" + self.services[name]["proxy_path"] return href def get_service_type(self, name, api_version="v1.0"): if not name in self.services: return RES_NOEXISTS return self.services[name]["type"] def list_resource(self, type, api_version="v1.0"): if not type in self.permitted_resources: return RES_UNSUPPORTED response = {} for name in self.services: response = (dict( response.items() + [(k, legalise_resource(x, type, api_version)) for (k, x) in self.services[name]["resource"][type].items() if ((api_version == "v1.0") or ("max_api_version" in x and not api_version_less_than( x["max_api_version"], api_version)))])) return response def _update_mdns(self, type): items = self.list_resource(type) if not isinstance(items, dict): return if len(items) == 1: try: self.mdns_updater.update_mdns(type, "register") except Exception: self.mdns_updater.update_mdns(type, "update") elif len(items) == 0: self.mdns_updater.update_mdns(type, "unregister") else: self.mdns_updater.update_mdns(type, "update") def list_self(self, api_version="v1.0"): return legalise_resource(self.node_data, "node", api_version) def update_ptp(self): do_update = False for clk in self.node_data['clocks']: if "ref_type" in clk and clk["ref_type"] == "ptp": old_clk = copy.copy(clk) clk['traceable'] = False clk['gmid'] = '00-00-00-00-00-00-00-00' clk['locked'] = False if clk != old_clk: do_update = True if do_update: self.update_node()
class FacadeRegistry(object): def __init__(self, resources, aggregator, mdns_updater, node_id, node_data, logger=None): # `node_data` must be correctly structured self.permitted_resources = resources self.services = {} self.aggregator = aggregator self.mdns_updater = mdns_updater self.node_id = node_id assert("interfaces" in node_data) # Check data conforms to latest supported API version self.node_data = node_data self.logger = Logger("facade_registry", logger) def modify_node(self, **kwargs): for key in kwargs.keys(): if key in self.node_data: self.node_data[key] = kwargs[key] self.update_node() def update_node(self): self.node_data["services"] = [] for service_name in self.services: href = None if self.services[service_name]["href"]: if self.services[service_name]["proxy_path"]: href = self.node_data["href"] + self.services[service_name]["proxy_path"] self.node_data["services"].append({"href": href, "type": self.services[service_name]["type"]}) self.node_data["version"] = str(ptptime.ptp_detail()[0]) + ":" + str(ptptime.ptp_detail()[1]) try: self.aggregator.register("node", self.node_id, **self.preprocess_resource("node", self.node_data["id"], self.node_data, NODE_REGVERSION)) except Exception as e: self.logger.writeError("Exception re-registering node: {}".format(e)) def register_service(self, name, srv_type, pid, href=None, proxy_path=None): if name in self.services: return RES_EXISTS self.services[name] = { "heartbeat": time.time(), "resource": {}, # Registered resources live under here "control": {}, # Registered device controls live under here "pid": pid, "href": href, "proxy_path": proxy_path, "type": srv_type } for resource_name in self.permitted_resources: self.services[name]["resource"][resource_name] = {} self.update_node() return RES_SUCCESS def update_service(self, name, pid, href=None, proxy_path=None): if not name in self.services: return RES_NOEXISTS if self.services[name]["pid"] != pid: return RES_UNAUTHORISED self.services[name]["heartbeat"] = time.time() self.services[name]["href"] = href self.services[name]["proxy_path"] = proxy_path self.update_node() return RES_SUCCESS def unregister_service(self, name, pid): if not name in self.services: return RES_NOEXISTS if self.services[name]["pid"] != pid: return RES_UNAUTHORISED for namespace in ["resource", "control"]: for type in self.services[name][namespace].keys(): for key in self.services[name][namespace][type].keys(): if namespace == "control": self._register(name, "control", pid, type, "remove", self.services[name][namespace][type][key]) else: self._unregister(name, namespace, pid, type, key) self.services.pop(name, None) self.update_node() return RES_SUCCESS def heartbeat_service(self, name, pid): if not name in self.services: return RES_NOEXISTS if self.services[name]["pid"] != pid: return RES_UNAUTHORISED self.services[name]["heartbeat"] = time.time() return RES_SUCCESS def cleanup_services(self): timed_out = time.time() - HEARTBEAT_TIMEOUT for name in self.services.keys(): if self.services[name]["heartbeat"] < timed_out: self.unregister_service(name, self.services[name]["pid"]) def register_resource(self, service_name, pid, type, key, value): if not type in self.permitted_resources: return RES_UNSUPPORTED return self._register(service_name, "resource", pid, type, key, value) def register_control(self, service_name, pid, device_id, control_data): return self._register(service_name, "control", pid, device_id, "add", control_data) def _register(self, service_name, namespace, pid, type, key, value): if namespace != "control": if "max_api_version" not in value: self.logger.writeWarning("Service {}: Registration without valid api version specified".format(service_name)) value["max_api_version"] = "v1.0" elif api_version_less_than(value["max_api_version"], NODE_REGVERSION): self.logger.writeWarning("Trying to register resource with api version too low: '{}' : {}".format(key, json.dumps(value))) if not service_name in self.services: return RES_NOEXISTS if not self.services[service_name]["pid"] == pid: return RES_UNAUTHORISED if key == "00000000-0000-0000-0000-000000000000": return RES_OTHERERROR # Add a node_id to those resources which need one if type == 'device': value['node_id'] = self.node_id if namespace == "control": if type not in self.services[service_name][namespace]: # 'type' is the Device ID in this case self.services[service_name][namespace][type] = {} if key == "add": # Register self.services[service_name][namespace][type][value["href"]] = value else: # Unregister self.services[service_name][namespace][type].pop(value["href"], None) # Reset the parameters below to force re-registration of the corresponding Device namespace = "resource" key = type # Device ID type = "device" value = None for name in self.services: # Find the service which registered the Device in question if key in self.services[name]["resource"][type]: value = self.services[name]["resource"][type][key] break if not value: # Device isn't actually registered at present return RES_SUCCESS else: self.services[service_name][namespace][type][key] = value # Don't pass non-registration exceptions to clients try: if namespace == "resource": self._update_mdns(type) except Exception as e: self.logger.writeError("Exception registering with mDNS: {}".format(e)) try: self.aggregator.register_into(namespace, type, key, **self.preprocess_resource(type, key, value, NODE_REGVERSION)) self.logger.writeDebug("registering {} {}".format(type, key)) except Exception as e: self.logger.writeError("Exception registering {}: {}".format(namespace, e)) return RES_OTHERERROR return RES_SUCCESS def update_resource(self, service_name, pid, type, key, value): return self.register_resource(service_name, pid, type, key, value) def find_service(self, type, key): for service_name in self.services.keys(): if key in self.services[service_name]["resource"][type]: return service_name return None def unregister_resource(self, service_name, pid, type, key): if not type in self.permitted_resources: return RES_UNSUPPORTED return self._unregister(service_name, "resource", pid, type, key) def unregister_control(self, service_name, pid, device_id, control_data): # Note use of register here, as we're updating an existing Device return self._register(service_name, "control", pid, device_id, "remove", control_data) def _unregister(self, service_name, namespace, pid, type, key): if not service_name in self.services: return RES_NOEXISTS if not self.services[service_name]["pid"] == pid: return RES_UNAUTHORISED if key == "00000000-0000-0000-0000-000000000000": return RES_OTHERERROR self.services[service_name][namespace][type].pop(key, None) # Don't pass non-registration exceptions to clients try: self.aggregator.unregister_from(namespace, type, key) except Exception as e: self.logger.writeError("Exception unregistering {}: {}".format(namespace, e)) return RES_OTHERERROR try: if namespace == "resource": self._update_mdns(type) except Exception as e: extype, exmsg = e self.logger.writeError("Exception unregistering from mDNS: {}".format(e)) if extype != -65548: # Name conflict return RES_OTHERERROR return RES_SUCCESS def list_services(self, api_version="v1.0"): return self.services.keys() def get_service_href(self, name, api_version="v1.0"): if not name in self.services: return RES_NOEXISTS href = self.services[name]["href"] if self.services[name]["proxy_path"]: href += "/" + self.services[name]["proxy_path"] return href def get_service_type(self, name, api_version="v1.0"): if not name in self.services: return RES_NOEXISTS return self.services[name]["type"] def preprocess_resource(self, type, key, value, api_version="v1.0"): if type == "device": value_copy = copy.deepcopy(value) for name in self.services: if key in self.services[name]["control"] and "controls" in value_copy: value_copy["controls"] = value_copy["controls"] + self.services[name]["control"][key].values() return legalise_resource(value_copy, type, api_version) else: return legalise_resource(value, type, api_version) def list_resource(self, type, api_version="v1.0"): if not type in self.permitted_resources: return RES_UNSUPPORTED response = {} for name in self.services: response = (dict(response.items() + [ (k, self.preprocess_resource(type, k, x, api_version)) for (k,x) in self.services[name]["resource"][type].items() if ((api_version == "v1.0") or ("max_api_version" in x and not api_version_less_than(x["max_api_version"], api_version))) ])) return response def _update_mdns(self, type): items = self.list_resource(type) if not isinstance(items, dict): return if len(items) == 1: try: self.mdns_updater.update_mdns(type, "register") except Exception: self.mdns_updater.update_mdns(type, "update") elif len(items) == 0: self.mdns_updater.update_mdns(type, "unregister") else: self.mdns_updater.update_mdns(type, "update") def list_self(self, api_version="v1.0"): return self.preprocess_resource("node", self.node_data["id"], self.node_data, api_version) def update_ptp(self): if IPP_UTILS_CLOCK_AVAILABLE: sts = IppClock().PTPStatus() do_update = False for clk in self.node_data['clocks']: old_clk = copy.copy(clk) if "ref_type" in clk and clk["ref_type"] == "ptp": clk['traceable'] = False clk['gmid'] = '00-00-00-00-00-00-00-00' clk['locked'] = False if IPP_UTILS_CLOCK_AVAILABLE: if len(sts.keys()) > 0: clk['traceable'] = sts['timeTraceable'] clk['gmid'] = sts['grandmasterClockIdentity'].lower() clk['locked'] = (sts['ofm'][0] == 0) if clk != old_clk: do_update = True if do_update: self.update_node()
class MDNSEngine(object): def __init__(self): self.running = False self.dnsServiceControllers = [] def start(self): if not self.running: self.logger = Logger('mdns-engine') self.subscriptionController = MDNSSubscriptionController() self.registrationController = MDNSRegistrationController() self.interfaceController = MDNSInterfaceController(self.logger) self.running = True def stop(self): self.close() def close(self): self.logger.writeDebug("MDNS Engine Closed") self.subscriptionController.close() self.registrationController.close() self.interfaceController.close() for controller in self.dnsServiceControllers: controller.close() self.running = False def run(self): pass def _autostart_if_required(self): if not self.running: self.start() def register(self, name, regtype, port, txtRecord="", callback=None, interfaceIps=None): self._autostart_if_required() callbackHandler = MDNSAdvertisementCallbackHandler( callback, regtype, name, port, txtRecord) if not interfaceIps: interfaceIps = [] try: interfaces = self.interfaceController.getInterfaces(interfaceIps) except InterfaceNotFoundException: callbackHandler.entryFailed() return registration = MDNSRegistration(interfaces, name, regtype, port, txtRecord) self._add_registration_handle_errors(registration, callbackHandler) def _add_registration_handle_errors(self, registration, callbackHandler): try: self.registrationController.addRegistration(registration) except zeroconf.NonUniqueNameException: callbackHandler.entryCollision() except ServiceAlreadyExistsException: callbackHandler.entryCollision() except zeroconf.Error as e: callbackHandler.entryFailed() print(str(e)) else: callbackHandler.entryEstablished() def update(self, name, regtype, txtRecord=None): self._autostart_if_required() try: registration = self.registrationController.registrations[regtype][ name] except KeyError: self.logger.writeError( "Could not update registraton type: {} with name {}" " - registration not found".format(regtype, name)) raise ServiceNotFoundException registration.update(name=name, regtype=regtype, txtRecord=txtRecord) def unregister(self, name, regType): self._autostart_if_required() self.registrationController.removeRegistration(name, regType) def callback_on_services(self, regtype, callback, registerOnly=True, domain=None): self._autostart_if_required() doDNSDiscover = config['dns_discover'] domDNSDiscover = config['mdns_discover'] if domDNSDiscover: listener = MDNSListener(callback, registerOnly) self.subscriptionController.addSubscription(listener, regtype) if doDNSDiscover: dnsServiceController = DNSServiceController( regtype, callback, self.logger, registerOnly, domain) dnsServiceController.start() self.dnsServiceControllers.append(dnsServiceController)