diff options
| author | Kim van der Riet <kpvdr@apache.org> | 2013-09-20 18:59:30 +0000 |
|---|---|---|
| committer | Kim van der Riet <kpvdr@apache.org> | 2013-09-20 18:59:30 +0000 |
| commit | c70bf3ea28cdf6bafd8571690d3e5c466a0658a2 (patch) | |
| tree | 68b24940e433f3f9c278b054d9ea1622389bd332 /qpid/extras/dispatch/python | |
| parent | fcdf1723c7b5cdf0772054a93edb6e7d97c4bb1e (diff) | |
| download | qpid-python-c70bf3ea28cdf6bafd8571690d3e5c466a0658a2.tar.gz | |
QPID-4984: WIP - Merge from trunk r.1525056
git-svn-id: https://svn.apache.org/repos/asf/qpid/branches/linearstore@1525101 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'qpid/extras/dispatch/python')
20 files changed, 2146 insertions, 0 deletions
diff --git a/qpid/extras/dispatch/python/qpid/__init__.py b/qpid/extras/dispatch/python/qpid/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/__init__.py diff --git a/qpid/extras/dispatch/python/qpid/dispatch/__init__.py b/qpid/extras/dispatch/python/qpid/dispatch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/__init__.py diff --git a/qpid/extras/dispatch/python/qpid/dispatch/config/__init__.py b/qpid/extras/dispatch/python/qpid/dispatch/config/__init__.py new file mode 100644 index 0000000000..36121bb015 --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/config/__init__.py @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +from parser import DXConfig diff --git a/qpid/extras/dispatch/python/qpid/dispatch/config/parser.py b/qpid/extras/dispatch/python/qpid/dispatch/config/parser.py new file mode 100644 index 0000000000..a809c78692 --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/config/parser.py @@ -0,0 +1,333 @@ +## +## Licensed to the Apache Software Foundation (ASF) under one +## or more contributor license agreements. See the NOTICE file +## distributed with this work for additional information +## regarding copyright ownership. The ASF licenses this file +## to you under the Apache License, Version 2.0 (the +## "License"); you may not use this file except in compliance +## with the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, +## software distributed under the License is distributed on an +## "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +## KIND, either express or implied. See the License for the +## specific language governing permissions and limitations +## under the License +## + +import json +from schema import config_schema +from dispatch import LogAdapter, LOG_TRACE, LOG_ERROR, LOG_INFO + +class Section: + """ + """ + + def __init__(self, name, kv_pairs, schema_section): + self.name = name + self.schema_section = schema_section + self.values = schema_section.check_and_default(kv_pairs) + self.index = schema_section.index_of(kv_pairs) + + def __repr__(self): + return "%r" % self.values + + +class ConfigMain: + """ + """ + + def __init__(self, schema): + self.sections_by_name = {} + self.sections_by_index = {} + self.schema = schema + + + def update(self, raw_config): + for sec_map in raw_config: + name = sec_map.keys()[0] + kv = sec_map[name] + schema_section = self.schema.sections[name] + sec = Section(name, kv, schema_section) + if name not in self.sections_by_name: + self.sections_by_name[name] = [] + self.sections_by_name[name].append(sec) + self.sections_by_index[sec.index] = sec + self._expand_references() + + + def item_count(self, name): + if name in self.sections_by_name: + return len(self.sections_by_name[name]) + return 0 + + + def get_value(self, name, idx, key): + if name in self.sections_by_name: + sec = self.sections_by_name[name] + if idx <= len(sec): + if key in sec[idx].values: + return sec[idx].values[key] + return None + + + def _expand_references(self): + for name, sec_list in self.sections_by_name.items(): + for sec in sec_list: + for k,v in sec.values.items(): + if sec.schema_section.is_expandable(k): + ref_name = "%s:%s" % (k, v) + if ref_name in self.sections_by_index: + ref_section = self.sections_by_index[ref_name] + for ek,ev in ref_section.values.items(): + if ref_section.schema_section.expand_copy(ek): + sec.values[ek] = ev + + +SECTION_SINGLETON = 0 +SECTION_VALUES = 1 + +VALUE_TYPE = 0 +VALUE_INDEX = 1 +VALUE_FLAGS = 2 +VALUE_DEFAULT = 3 + + +class SchemaSection: + """ + """ + + def __init__(self, name, section_tuple): + self.name = name + self.singleton = section_tuple[SECTION_SINGLETON] + self.values = section_tuple[SECTION_VALUES] + self.index_keys = [] + finding_index = True + index_ord = 0 + while finding_index: + finding_index = False + for k,v in self.values.items(): + if v[VALUE_INDEX] == index_ord: + self.index_keys.append(k) + index_ord += 1 + finding_index = True + + + def is_mandatory(self, key): + return self.values[key][VALUE_FLAGS].find('M') >= 0 + + + def is_expandable(self, key): + return self.values[key][VALUE_FLAGS].find('E') >= 0 + + + def expand_copy(self, key): + return self.values[key][VALUE_FLAGS].find('S') >= 0 + + + def default_value(self, key): + return self.values[key][VALUE_DEFAULT] + + + def check_and_default(self, kv_map): + copy = {} + for k,v in self.values.items(): + if k not in kv_map: + if self.is_mandatory(k): + raise Exception("In section '%s', missing mandatory key '%s'" % (self.name, k)) + else: + copy[k] = self.default_value(k) + for k,v in kv_map.items(): + if k not in self.values: + raise Exception("In section '%s', unknown key '%s'" % (self.name, k)) + copy[k] = v + return copy + + + def index_of(self, kv_map): + result = self.name + for key in self.index_keys: + result += ':%s' % kv_map[key] + if result == "": + result = "SINGLE" + return result + + +class Schema: + """ + """ + + def __init__(self): + self.sections = {} + for k,v in config_schema.items(): + self.sections[k] = SchemaSection(k, v) + + + +class DXConfig: + """ + Configuration File Parser for Qpid Dispatch + + Configuration files are made up of "sections" having the following form: + + section-name { + key0: value0 + key1: value1 + ... + keyN: valueN + } + + Sections may be repeated (i.e. there may be multiple instances with the same section name). + The keys must be unique within a section. Values can be of string or integer types. No + quoting is necessary anywhere in the configuration file. Values may contain whitespace. + + Comment lines starting with the '#' character will be ignored. + + This parser converts the configuration file into a json string where the file is represented + as a list of maps. Each map has one item, the key being the section name and the value being + a nested map of keys and values from the file. This json string is parsed into a data + structure that may then be queried. + + """ + + def __init__(self, path): + self.path = path + self.raw_config = None + self.config = None + self.schema = Schema() + self.log = LogAdapter('config.parser') + + + def read_file(self): + try: + self.log.log(LOG_INFO, "Reading Configuration File: %s" % self.path) + cfile = open(self.path) + text = cfile.read() + cfile.close() + + self.json_text = "[" + self._toJson(text) + "]" + self.raw_config = json.loads(self.json_text); + self._validate_raw_config() + self._process_schema() + except Exception, E: + print "Exception in read_file: %r" % E + raise + + + def __repr__(self): + return "%r" % self.config + + + def _toJson(self, text): + lines = text.split('\n') + stripped = "" + for line in lines: + sline = line.strip() + + # + # Ignore empty lines + # + if len(sline) == 0: + continue + + # + # Ignore comment lines + # + if sline.find('#') == 0: + continue + + # + # Convert section opens, closes, and colon-separated key:value lines into json + # + if sline[-1:] == '{': + sline = '{"' + sline[:-1].strip() + '" : {' + elif sline == '}': + sline = '}},' + else: + colon = sline.find(':') + if colon > 1: + sline = '"' + sline[:colon] + '":"' + sline[colon+1:].strip() + '",' + stripped += sline + + # + # Remove the trailing commas in map entries + # + stripped = stripped.replace(",}", "}") + + # + # Return the entire document minus the trailing comma + # + return stripped[:-1] + + + def _validate_raw_config(self): + """ + Ensure that the configuration is well-formed. Once this is validated, + further functions can assume a well-formed data structure is in place. + """ + if self.raw_config.__class__ != list: + raise Exception("Invalid Config: Expected List at top level") + for section in self.raw_config: + if section.__class__ != dict: + raise Exception("Invalid Config: List items must be maps") + if len(section) != 1: + raise Exception("Invalid Config: Map must have only one entry") + for key,val in section.items(): + if key.__class__ != str and key.__class__ != unicode: + raise Exception("Invalid Config: Key in map must be a string") + if val.__class__ != dict: + raise Exception("Invalid Config: Value in map must be a map") + for k,v in val.items(): + if k.__class__ != str and k.__class__ != unicode: + raise Exception("Invalid Config: Key in section must be a string") + if v.__class__ != str and v.__class__ != unicode: + raise Exception("Invalid Config: Value in section must be a string") + + + def _process_schema(self): + self.config = ConfigMain(self.schema) + self.config.update(self.raw_config) + self.raw_config = None + + + def item_count(self, section): + """ + Return the number of items in a section (i.e. the number if instances of a section-name). + """ + result = self.config.item_count(section) + return result + + + def _value(self, section, idx, key): + return self.config.get_value(section, idx, key) + + + def value_string(self, section, idx, key): + """ + Return the string value for the key in the idx'th item in the section. + """ + value = self._value(section, idx, key) + if value: + return str(value) + return None + + + def value_int(self, section, idx, key): + """ + Return the integer value for the key in the idx'th item in the section. + """ + value = self._value(section, idx, key) + return long(value) + + + def value_bool(self, section, idx, key): + """ + Return the boolean value for the key in the idx'th item in the section. + """ + value = self._value(section, idx, key) + if value: + if str(value) != "no": + return True + return None diff --git a/qpid/extras/dispatch/python/qpid/dispatch/config/schema.py b/qpid/extras/dispatch/python/qpid/dispatch/config/schema.py new file mode 100644 index 0000000000..545139f0df --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/config/schema.py @@ -0,0 +1,82 @@ +## +## Licensed to the Apache Software Foundation (ASF) under one +## or more contributor license agreements. See the NOTICE file +## distributed with this work for additional information +## regarding copyright ownership. The ASF licenses this file +## to you under the Apache License, Version 2.0 (the +## "License"); you may not use this file except in compliance +## with the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, +## software distributed under the License is distributed on an +## "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +## KIND, either express or implied. See the License for the +## specific language governing permissions and limitations +## under the License +## + +# +# config_schema = +# { <section_name> : +# (<singleton>, +# {<key> : (<value-type>, <index>, <flags>, <default-value>) +# ) +# } +# +# <section-name> = String name of a configuration section +# <singleton> = False => There may be 0 or more sections with this name +# True => There must be exactly one section with this name +# <key> = String key of a section's key-value pair +# <value-type> = Python type for the value +# <index> = None => This value is not an index for multiple sections +# >= 0 => Ordinal of this value in the section primary-key +# <flags> = Set of characters: +# M = Mandatory (no default value) +# E = Expand referenced section into this record +# S = During expansion, this key should be copied +# <default-value> = If not mandatory and not specified, the value defaults to this +# value +# + +config_schema = { + 'container' : (True, { + 'worker-threads' : (int, None, "", 1), + 'container-name' : (str, None, "", None) + }), + 'ssl-profile' : (False, { + 'name' : (str, 0, "M"), + 'cert-db' : (str, None, "S", None), + 'cert-file' : (str, None, "S", None), + 'key-file' : (str, None, "S", None), + 'password-file' : (str, None, "S", None), + 'password' : (str, None, "S", None) + }), + 'listener' : (False, { + 'addr' : (str, 0, "M"), + 'port' : (str, 1, "M"), + 'label' : (str, None, "", None), + 'sasl-mechanisms' : (str, None, "M"), + 'ssl-profile' : (str, None, "E", None), + 'require-peer-auth' : (bool, None, "", True), + 'allow-unsecured' : (bool, None, "", False) + }), + 'connector' : (False, { + 'addr' : (str, 0, "M"), + 'port' : (str, 1, "M"), + 'label' : (str, None, "", None), + 'sasl-mechanisms' : (str, None, "M"), + 'ssl-profile' : (str, None, "E", None), + 'allow-redirect' : (bool, None, "", True) + }), + 'router' : (True, { + 'router-id' : (str, None, "M"), + 'area' : (str, None, "", None), + 'hello-interval' : (int, None, "", 1), + 'hello-max-age' : (int, None, "", 3), + 'ra-interval' : (int, None, "", 30), + 'remote-ls-max-age' : (int, None, "", 60), + 'mobile-addr-max-age' : (int, None, "", 60) + })} + diff --git a/qpid/extras/dispatch/python/qpid/dispatch/router/__init__.py b/qpid/extras/dispatch/python/qpid/dispatch/router/__init__.py new file mode 100644 index 0000000000..bdb26a0c11 --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/router/__init__.py @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +from .router_engine import * diff --git a/qpid/extras/dispatch/python/qpid/dispatch/router/adapter.py b/qpid/extras/dispatch/python/qpid/dispatch/router/adapter.py new file mode 100644 index 0000000000..7f7f6c9e8e --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/router/adapter.py @@ -0,0 +1,99 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +try: + from dispatch import * +except ImportError: + from ..stubs import * + +ENTRY_OLD = 1 +ENTRY_CURRENT = 2 +ENTRY_NEW = 3 + +class AdapterEngine(object): + """ + This module is responsible for managing the Adapter's key bindings (list of address-subject:next-hop). + Key binding lists are kept in disjoint key-classes that can come from different parts of the router + (i.e. topological keys for inter-router communication and mobile keys for end users). + + For each key-class, a mirror copy of what the adapter has is kept internally. This allows changes to the + routing tables to be efficiently communicated to the adapter in the form of table deltas. + """ + def __init__(self, container): + self.container = container + self.id = self.container.id + self.area = self.container.area + self.key_classes = {} # map [key_class] => (addr-key, next-hop) + + + def tick(self, now): + """ + There is no periodic processing needed for this module. + """ + pass + + + def remote_routes_changed(self, key_class, new_table): + old_table = [] + if key_class in self.key_classes: + old_table = self.key_classes[key_class] + + # flag all of the old entries + old_flags = {} + for a,b in old_table: + old_flags[(a,b)] = ENTRY_OLD + + # flag the new entries + new_flags = {} + for a,b in new_table: + new_flags[(a,b)] = ENTRY_NEW + + # calculate the differences from old to new + for a,b in new_table: + if old_table.count((a,b)) > 0: + old_flags[(a,b)] = ENTRY_CURRENT + new_flags[(a,b)] = ENTRY_CURRENT + + # make to_add and to_delete lists + to_add = [] + to_delete = [] + for (a,b),f in old_flags.items(): + if f == ENTRY_OLD: + to_delete.append((a,b)) + for (a,b),f in new_flags.items(): + if f == ENTRY_NEW: + to_add.append((a,b)) + + # set the routing table to the new contents + self.key_classes[key_class] = new_table + + # update the adapter's routing tables + # Note: Do deletions before adds to avoid overlapping routes that may cause + # messages to be duplicated. It's better to have gaps in the routing + # tables momentarily because unroutable messages are stored for retry. + for a,b in to_delete: + self.container.router_adapter.remote_unbind(a, b) + for a,b in to_add: + self.container.router_adapter.remote_bind(a, b) + + self.container.log(LOG_INFO, "New Routing Table (class=%s):" % key_class) + for a,b in new_table: + self.container.log(LOG_INFO, " %s => %s" % (a, b)) + + diff --git a/qpid/extras/dispatch/python/qpid/dispatch/router/binding.py b/qpid/extras/dispatch/python/qpid/dispatch/router/binding.py new file mode 100644 index 0000000000..a37b585e3e --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/router/binding.py @@ -0,0 +1,133 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +try: + from dispatch import * +except ImportError: + from ..stubs import * + + +class BindingEngine(object): + """ + This module is responsible for responding to two different events: + 1) The learning of new remote mobile addresses + 2) The change of topology (i.e. different next-hops for remote routers) + When these occur, this module converts the mobile routing table (address => router) + to a next-hop routing table (address => next-hop), compresses the keys in case there + are wild-card overlaps, and notifies outbound of changes in the "mobile-key" address class. + """ + def __init__(self, container): + self.container = container + self.id = self.container.id + self.area = self.container.area + self.current_keys = {} + + + def tick(self, now): + pass + + + def mobile_keys_changed(self, keys): + self.current_keys = keys + next_hop_keys = self._convert_ids_to_next_hops(keys) + routing_table = self._compress_keys(next_hop_keys) + self.container.remote_routes_changed('mobile-key', routing_table) + + + def next_hops_changed(self): + next_hop_keys = self._convert_ids_to_next_hops(self.current_keys) + routing_table = self._compress_keys(next_hop_keys) + self.container.remote_routes_changed('mobile-key', routing_table) + + + def _convert_ids_to_next_hops(self, keys): + next_hops = self.container.get_next_hops() + new_keys = {} + for _id, value in keys.items(): + if _id in next_hops: + next_hop = next_hops[_id] + if next_hop not in new_keys: + new_keys[next_hop] = [] + new_keys[next_hop].extend(value) + return new_keys + + + def _compress_keys(self, keys): + trees = {} + for _id, key_list in keys.items(): + trees[_id] = TopicElementList() + for key in key_list: + trees[_id].add_key(key) + routing_table = [] + for _id, tree in trees.items(): + tree_keys = tree.get_list() + for tk in tree_keys: + routing_table.append((tk, _id)) + return routing_table + + +class TopicElementList(object): + """ + """ + def __init__(self): + self.elements = {} # map text => (terminal, sub-list) + + def __repr__(self): + return "%r" % self.elements + + def add_key(self, key): + self.add_tokens(key.split('.')) + + def add_tokens(self, tokens): + first = tokens.pop(0) + terminal = len(tokens) == 0 + + if terminal and first == '#': + ## Optimization #1A (A.B.C.D followed by A.B.#) + self.elements = {'#':(True, TopicElementList())} + return + + if '#' in self.elements: + _t,_el = self.elements['#'] + if _t: + ## Optimization #1B (A.B.# followed by A.B.C.D) + return + + if first not in self.elements: + self.elements[first] = (terminal, TopicElementList()) + else: + _t,_el = self.elements[first] + if terminal and not _t: + self.elements[first] = (terminal, _el) + + if not terminal: + _t,_el = self.elements[first] + _el.add_tokens(tokens) + + def get_list(self): + keys = [] + for token, (_t,_el) in self.elements.items(): + if _t: keys.append(token) + _el.build_list(token, keys) + return keys + + def build_list(self, prefix, keys): + for token, (_t,_el) in self.elements.items(): + if _t: keys.append("%s.%s" % (prefix, token)) + _el.build_list("%s.%s" % (prefix, token), keys) diff --git a/qpid/extras/dispatch/python/qpid/dispatch/router/configuration.py b/qpid/extras/dispatch/python/qpid/dispatch/router/configuration.py new file mode 100644 index 0000000000..f87d2ee7d2 --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/router/configuration.py @@ -0,0 +1,47 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +class Configuration(object): + """ + This module manages and holds the configuration and tuning parameters for a router. + """ + def __init__(self, overrides={}): + ## + ## Load default values + ## + self.values = { 'hello_interval' : 1.0, + 'hello_max_age' : 3.0, + 'ra_interval' : 30.0, + 'remote_ls_max_age' : 60.0, + 'mobile_addr_max_age' : 60.0 } + + ## + ## Apply supplied overrides + ## + for k, v in overrides.items(): + self.values[k] = v + + def __getattr__(self, key): + if key in self.values: + return self.values[key] + raise KeyError + + def __repr__(self): + return "%r" % self.values + diff --git a/qpid/extras/dispatch/python/qpid/dispatch/router/data.py b/qpid/extras/dispatch/python/qpid/dispatch/router/data.py new file mode 100644 index 0000000000..89e347a29e --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/router/data.py @@ -0,0 +1,275 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + + +try: + from dispatch import * +except ImportError: + from ..stubs import * + + +def getMandatory(data, key, cls=None): + """ + Get the value mapped to the requested key. If it's not present, raise an exception. + """ + if key in data: + value = data[key] + if cls and value.__class__ != cls: + raise Exception("Protocol field has wrong data type: '%s' type=%r expected=%r" % (key, value.__class__, cls)) + return value + raise Exception("Mandatory protocol field missing: '%s'" % key) + + +def getOptional(data, key, default=None, cls=None): + """ + Get the value mapped to the requested key. If it's not present, return the default value. + """ + if key in data: + value = data[key] + if cls and value.__class__ != cls: + raise Exception("Protocol field has wrong data type: '%s' type=%r expected=%r" % (key, value.__class__, cls)) + return value + return default + + +class LinkState(object): + """ + The link-state of a single router. The link state consists of a list of neighbor routers reachable from + the reporting router. The link-state-sequence number is incremented each time the link state changes. + """ + def __init__(self, body, _id=None, _area=None, _ls_seq=None, _peers=None): + self.last_seen = 0 + if body: + self.id = getMandatory(body, 'id', str) + self.area = getMandatory(body, 'area', str) + self.ls_seq = getMandatory(body, 'ls_seq', long) + self.peers = getMandatory(body, 'peers', list) + else: + self.id = _id + self.area = _area + self.ls_seq = long(_ls_seq) + self.peers = _peers + + def __repr__(self): + return "LS(id=%s area=%s ls_seq=%d peers=%r)" % (self.id, self.area, self.ls_seq, self.peers) + + def to_dict(self): + return {'id' : self.id, + 'area' : self.area, + 'ls_seq' : self.ls_seq, + 'peers' : self.peers} + + def add_peer(self, _id): + if self.peers.count(_id) == 0: + self.peers.append(_id) + return True + return False + + def del_peer(self, _id): + if self.peers.count(_id) > 0: + self.peers.remove(_id) + return True + return False + + def bump_sequence(self): + self.ls_seq += 1 + + +class MessageHELLO(object): + """ + HELLO Message + scope: neighbors only - HELLO messages travel at most one hop + This message is used by directly connected routers to determine with whom they have + bidirectional connectivity. + """ + def __init__(self, body, _id=None, _area=None, _seen_peers=None): + if body: + self.id = getMandatory(body, 'id', str) + self.area = getMandatory(body, 'area', str) + self.seen_peers = getMandatory(body, 'seen', list) + else: + self.id = _id + self.area = _area + self.seen_peers = _seen_peers + + def __repr__(self): + return "HELLO(id=%s area=%s seen=%r)" % (self.id, self.area, self.seen_peers) + + def get_opcode(self): + return 'HELLO' + + def to_dict(self): + return {'id' : self.id, + 'area' : self.area, + 'seen' : self.seen_peers} + + def is_seen(self, _id): + return self.seen_peers.count(_id) > 0 + + +class MessageRA(object): + """ + Router Advertisement (RA) Message + scope: all routers in the area and all designated routers + This message is sent periodically to indicate the originating router's sequence numbers + for link-state and mobile-address-state. + """ + def __init__(self, body, _id=None, _area=None, _ls_seq=None, _mobile_seq=None): + if body: + self.id = getMandatory(body, 'id', str) + self.area = getMandatory(body, 'area', str) + self.ls_seq = getMandatory(body, 'ls_seq', long) + self.mobile_seq = getMandatory(body, 'mobile_seq', long) + else: + self.id = _id + self.area = _area + self.ls_seq = long(_ls_seq) + self.mobile_seq = long(_mobile_seq) + + def get_opcode(self): + return 'RA' + + def __repr__(self): + return "RA(id=%s area=%s ls_seq=%d mobile_seq=%d)" % \ + (self.id, self.area, self.ls_seq, self.mobile_seq) + + def to_dict(self): + return {'id' : self.id, + 'area' : self.area, + 'ls_seq' : self.ls_seq, + 'mobile_seq' : self.mobile_seq} + + +class MessageLSU(object): + """ + """ + def __init__(self, body, _id=None, _area=None, _ls_seq=None, _ls=None): + if body: + self.id = getMandatory(body, 'id', str) + self.area = getMandatory(body, 'area', str) + self.ls_seq = getMandatory(body, 'ls_seq', long) + self.ls = LinkState(getMandatory(body, 'ls', dict)) + else: + self.id = _id + self.area = _area + self.ls_seq = long(_ls_seq) + self.ls = _ls + + def get_opcode(self): + return 'LSU' + + def __repr__(self): + return "LSU(id=%s area=%s ls_seq=%d ls=%r)" % \ + (self.id, self.area, self.ls_seq, self.ls) + + def to_dict(self): + return {'id' : self.id, + 'area' : self.area, + 'ls_seq' : self.ls_seq, + 'ls' : self.ls.to_dict()} + + +class MessageLSR(object): + """ + """ + def __init__(self, body, _id=None, _area=None): + if body: + self.id = getMandatory(body, 'id', str) + self.area = getMandatory(body, 'area', str) + else: + self.id = _id + self.area = _area + + def get_opcode(self): + return 'LSR' + + def __repr__(self): + return "LSR(id=%s area=%s)" % (self.id, self.area) + + def to_dict(self): + return {'id' : self.id, + 'area' : self.area} + + +class MessageMAU(object): + """ + """ + def __init__(self, body, _id=None, _area=None, _seq=None, _add_list=None, _del_list=None, _exist_list=None): + if body: + self.id = getMandatory(body, 'id', str) + self.area = getMandatory(body, 'area', str) + self.mobile_seq = getMandatory(body, 'mobile_seq', long) + self.add_list = getOptional(body, 'add', None, list) + self.del_list = getOptional(body, 'del', None, list) + self.exist_list = getOptional(body, 'exist', None, list) + else: + self.id = _id + self.area = _area + self.mobile_seq = long(_seq) + self.add_list = _add_list + self.del_list = _del_list + self.exist_list = _exist_list + + def get_opcode(self): + return 'MAU' + + def __repr__(self): + _add = '' + _del = '' + _exist = '' + if self.add_list: _add = ' add=%r' % self.add_list + if self.del_list: _del = ' del=%r' % self.del_list + if self.exist_list: _exist = ' exist=%r' % self.exist_list + return "MAU(id=%s area=%s mobile_seq=%d%s%s%s)" % \ + (self.id, self.area, self.mobile_seq, _add, _del, _exist) + + def to_dict(self): + body = { 'id' : self.id, + 'area' : self.area, + 'mobile_seq' : self.mobile_seq } + if self.add_list: body['add'] = self.add_list + if self.del_list: body['del'] = self.del_list + if self.exist_list: body['exist'] = self.exist_list + return body + + +class MessageMAR(object): + """ + """ + def __init__(self, body, _id=None, _area=None, _have_seq=None): + if body: + self.id = getMandatory(body, 'id', str) + self.area = getMandatory(body, 'area', str) + self.have_seq = getMandatory(body, 'have_seq', long) + else: + self.id = _id + self.area = _area + self.have_seq = long(_have_seq) + + def get_opcode(self): + return 'MAR' + + def __repr__(self): + return "MAR(id=%s area=%s have_seq=%d)" % (self.id, self.area, self.have_seq) + + def to_dict(self): + return {'id' : self.id, + 'area' : self.area, + 'have_seq' : self.have_seq} + diff --git a/qpid/extras/dispatch/python/qpid/dispatch/router/link.py b/qpid/extras/dispatch/python/qpid/dispatch/router/link.py new file mode 100644 index 0000000000..fb23177e2f --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/router/link.py @@ -0,0 +1,143 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +from data import MessageRA, MessageLSU, MessageLSR +from time import time + +try: + from dispatch import * +except ImportError: + from ..stubs import * + +class LinkStateEngine(object): + """ + This module is responsible for running the Link State protocol and maintaining the set + of link states that are gathered from the domain. It notifies outbound when changes to + the link-state-collection are detected. + """ + def __init__(self, container): + self.container = container + self.id = self.container.id + self.area = self.container.area + self.ra_interval = self.container.config.ra_interval + self.remote_ls_max_age = self.container.config.remote_ls_max_age + self.last_ra_time = 0 + self.collection = {} + self.collection_changed = False + self.mobile_seq = 0 + self.needed_lsrs = {} + + + def tick(self, now): + self._expire_ls(now) + self._send_lsrs() + + if now - self.last_ra_time >= self.ra_interval: + self.last_ra_time = now + self._send_ra() + + if self.collection_changed: + self.collection_changed = False + self.container.log(LOG_INFO, "New Link-State Collection:") + for a,b in self.collection.items(): + self.container.log(LOG_INFO, " %s => %r" % (a, b.peers)) + self.container.ls_collection_changed(self.collection) + + + def handle_ra(self, msg, now): + if msg.id == self.id: + return + if msg.id in self.collection: + ls = self.collection[msg.id] + ls.last_seen = now + if ls.ls_seq < msg.ls_seq: + self.needed_lsrs[(msg.area, msg.id)] = None + else: + self.needed_lsrs[(msg.area, msg.id)] = None + + + def handle_lsu(self, msg, now): + if msg.id == self.id: + return + if msg.id in self.collection: + ls = self.collection[msg.id] + if ls.ls_seq < msg.ls_seq: + ls = msg.ls + self.collection[msg.id] = ls + self.collection_changed = True + ls.last_seen = now + else: + ls = msg.ls + self.collection[msg.id] = ls + self.collection_changed = True + ls.last_seen = now + self.container.new_node(msg.id) + self.container.log(LOG_INFO, "Learned link-state from new router: %s" % msg.id) + # Schedule LSRs for any routers referenced in this LS that we don't know about + for _id in msg.ls.peers: + if _id not in self.collection: + self.needed_lsrs[(msg.area, _id)] = None + + + def handle_lsr(self, msg, now): + if msg.id == self.id: + return + if self.id not in self.collection: + return + my_ls = self.collection[self.id] + self.container.send('_topo/%s/%s' % (msg.area, msg.id), MessageLSU(None, self.id, self.area, my_ls.ls_seq, my_ls)) + + + def new_local_link_state(self, link_state): + self.collection[self.id] = link_state + self.collection_changed = True + self._send_ra() + + + def set_mobile_sequence(self, seq): + self.mobile_seq = seq + + + def get_collection(self): + return self.collection + + + def _expire_ls(self, now): + to_delete = [] + for key, ls in self.collection.items(): + if key != self.id and now - ls.last_seen > self.remote_ls_max_age: + to_delete.append(key) + for key in to_delete: + ls = self.collection.pop(key) + self.collection_changed = True + self.container.lost_node(key) + self.container.log(LOG_INFO, "Expired link-state from router: %s" % key) + + + def _send_lsrs(self): + for (_area, _id) in self.needed_lsrs.keys(): + self.container.send('_topo/%s/%s' % (_area, _id), MessageLSR(None, self.id, self.area)) + self.needed_lsrs = {} + + + def _send_ra(self): + ls_seq = 0 + if self.id in self.collection: + ls_seq = self.collection[self.id].ls_seq + self.container.send('_topo/%s/all' % self.area, MessageRA(None, self.id, self.area, ls_seq, self.mobile_seq)) diff --git a/qpid/extras/dispatch/python/qpid/dispatch/router/mobile.py b/qpid/extras/dispatch/python/qpid/dispatch/router/mobile.py new file mode 100644 index 0000000000..ce80f38780 --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/router/mobile.py @@ -0,0 +1,188 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +from data import MessageRA, MessageMAR, MessageMAU + +try: + from dispatch import * +except ImportError: + from ..stubs import * + +class MobileAddressEngine(object): + """ + This module is responsible for maintaining an up-to-date list of mobile addresses in the domain. + It runs the Mobile-Address protocol and generates an un-optimized routing table for mobile addresses. + Note that this routing table maps from the mobile address to the remote router where that address + is directly bound. + """ + def __init__(self, container): + self.container = container + self.id = self.container.id + self.area = self.container.area + self.mobile_addr_max_age = self.container.config.mobile_addr_max_age + self.mobile_seq = 0 + self.local_keys = [] + self.added_keys = [] + self.deleted_keys = [] + self.remote_lists = {} # map router_id => (sequence, list of keys) + self.remote_last_seen = {} # map router_id => time of last seen advertizement/update + self.remote_changed = False + self.needed_mars = {} + + + def tick(self, now): + self._expire_remotes(now) + self._send_mars() + + ## + ## If local keys have changed, collect the changes and send a MAU with the diffs + ## Note: it is important that the differential-MAU be sent before a RA is sent + ## + if len(self.added_keys) > 0 or len(self.deleted_keys) > 0: + self.mobile_seq += 1 + self.container.send('_topo.%s.all' % self.area, + MessageMAU(None, self.id, self.area, self.mobile_seq, self.added_keys, self.deleted_keys)) + self.local_keys.extend(self.added_keys) + for key in self.deleted_keys: + self.local_keys.remove(key) + self.added_keys = [] + self.deleted_keys = [] + self.container.mobile_sequence_changed(self.mobile_seq) + + ## + ## If remotes have changed, start the process of updating local bindings + ## + if self.remote_changed: + self.remote_changed = False + self._update_remote_keys() + + + def add_local_address(self, key): + """ + """ + if self.local_keys.count(key) == 0: + if self.added_keys.count(key) == 0: + self.added_keys.append(key) + else: + if self.deleted_keys.count(key) > 0: + self.deleted_keys.remove(key) + + + def del_local_address(self, key): + """ + """ + if self.local_keys.count(key) > 0: + if self.deleted_keys.count(key) == 0: + self.deleted_keys.append(key) + else: + if self.added_keys.count(key) > 0: + self.added_keys.remove(key) + + + def handle_ra(self, msg, now): + if msg.id == self.id: + return + + if msg.mobile_seq == 0: + return + + if msg.id in self.remote_lists: + _seq, _list = self.remote_lists[msg.id] + self.remote_last_seen[msg.id] = now + if _seq < msg.mobile_seq: + self.needed_mars[(msg.id, msg.area, _seq)] = None + else: + self.needed_mars[(msg.id, msg.area, 0)] = None + + + def handle_mau(self, msg, now): + ## + ## If the MAU is differential, we can only use it if its sequence is exactly one greater + ## than our stored sequence. If not, we will ignore the content and schedule a MAR. + ## + ## If the MAU is absolute, we can use it in all cases. + ## + if msg.id == self.id: + return + + if msg.exist_list: + ## + ## Absolute MAU + ## + if msg.id in self.remote_lists: + _seq, _list = self.remote_lists[msg.id] + if _seq >= msg.mobile_seq: # ignore duplicates + return + self.remote_lists[msg.id] = (msg.mobile_seq, msg.exist_list) + self.remote_last_seen[msg.id] = now + self.remote_changed = True + else: + ## + ## Differential MAU + ## + if msg.id in self.remote_lists: + _seq, _list = self.remote_lists[msg.id] + if _seq == msg.mobile_seq: # ignore duplicates + return + self.remote_last_seen[msg.id] = now + if _seq + 1 == msg.mobile_seq: + ## + ## This is one greater than our stored value, incorporate the deltas + ## + if msg.add_list and msg.add_list.__class__ == list: + _list.extend(msg.add_list) + if msg.del_list and msg.del_list.__class__ == list: + for key in msg.del_list: + _list.remove(key) + self.remote_lists[msg.id] = (msg.mobile_seq, _list) + self.remote_changed = True + else: + self.needed_mars[(msg.id, msg.area, _seq)] = None + else: + self.needed_mars[(msg.id, msg.area, 0)] = None + + + def handle_mar(self, msg, now): + if msg.id == self.id: + return + if msg.have_seq < self.mobile_seq: + self.container.send('_topo.%s.%s' % (msg.area, msg.id), + MessageMAU(None, self.id, self.area, self.mobile_seq, None, None, self.local_keys)) + + + def _update_remote_keys(self): + keys = {} + for _id,(seq,key_list) in self.remote_lists.items(): + keys[_id] = key_list + self.container.mobile_keys_changed(keys) + + + def _expire_remotes(self, now): + for _id, t in self.remote_last_seen.items(): + if now - t > self.mobile_addr_max_age: + self.remote_lists.pop(_id) + self.remote_last_seen.pop(_id) + self.remote_changed = True + + + def _send_mars(self): + for _id, _area, _seq in self.needed_mars.keys(): + self.container.send('_topo.%s.%s' % (_area, _id), MessageMAR(None, self.id, self.area, _seq)) + self.needed_mars = {} + diff --git a/qpid/extras/dispatch/python/qpid/dispatch/router/neighbor.py b/qpid/extras/dispatch/python/qpid/dispatch/router/neighbor.py new file mode 100644 index 0000000000..ea8bacd660 --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/router/neighbor.py @@ -0,0 +1,83 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +from data import LinkState, MessageHELLO +from time import time + +try: + from dispatch import * +except ImportError: + from ..stubs import * + + +class NeighborEngine(object): + """ + This module is responsible for maintaining this router's link-state. It runs the HELLO protocol + with the router's neighbors and notifies outbound when the list of neighbors-in-good-standing (the + link-state) changes. + """ + def __init__(self, container): + self.container = container + self.id = self.container.id + self.area = self.container.area + self.last_hello_time = 0.0 + self.hello_interval = container.config.hello_interval + self.hello_max_age = container.config.hello_max_age + self.hellos = {} + self.link_state_changed = False + self.link_state = LinkState(None, self.id, self.area, 0, []) + + + def tick(self, now): + self._expire_hellos(now) + + if now - self.last_hello_time >= self.hello_interval: + self.last_hello_time = now + self.container.send('_local/qdxrouter', MessageHELLO(None, self.id, self.area, self.hellos.keys())) + + if self.link_state_changed: + self.link_state_changed = False + self.link_state.bump_sequence() + self.container.local_link_state_changed(self.link_state) + + + def handle_hello(self, msg, now): + if msg.id == self.id: + return + self.hellos[msg.id] = now + if msg.is_seen(self.id): + if self.link_state.add_peer(msg.id): + self.link_state_changed = True + self.container.new_neighbor(msg.id) + self.container.log(LOG_INFO, "New neighbor established: %s" % msg.id) + ## + ## TODO - Use this function to detect area boundaries + ## + + def _expire_hellos(self, now): + to_delete = [] + for key, last_seen in self.hellos.items(): + if now - last_seen > self.hello_max_age: + to_delete.append(key) + for key in to_delete: + self.hellos.pop(key) + if self.link_state.del_peer(key): + self.link_state_changed = True + self.container.lost_neighbor(key) + self.container.log(LOG_INFO, "Neighbor lost: %s" % key) diff --git a/qpid/extras/dispatch/python/qpid/dispatch/router/node.py b/qpid/extras/dispatch/python/qpid/dispatch/router/node.py new file mode 100644 index 0000000000..c90f7f4232 --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/router/node.py @@ -0,0 +1,103 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +try: + from dispatch import * +except ImportError: + from ..stubs import * + + +class NodeTracker(object): + """ + This module is responsible for tracking the set of router nodes that are known to this + router. It tracks whether they are neighbor or remote and whether they are reachable. + """ + def __init__(self, container): + self.container = container + self.id = self.container.id + self.area = self.container.area + self.nodes = {} # id => RemoteNode + + + def tick(self, now): + pass + + + def new_neighbor(self, node_id): + if node_id not in self.nodes: + self.nodes[node_id] = RemoteNode(node_id) + self.nodes[node_id].set_neighbor() + self._notify(self.nodes[node_id]) + + + def lost_neighbor(self, node_id): + node = self.nodes[node_id] + node.clear_neighbor() + self._notify(node) + if node.to_delete(): + self.nodes.pop(node_id) + + + def new_node(self, node_id): + if node_id not in self.nodes: + self.nodes[node_id] = RemoteNode(node_id) + self.nodes[node_id].set_remote() + self._notify(self.nodes[node_id]) + + + def lost_node(self, node_id): + node = self.nodes[node_id] + node.clear_remote() + self._notify(node) + if node.to_delete(): + self.nodes.pop(node_id) + + + def _notify(self, node): + if node.to_delete(): + self.container.adapter.node_updated("R%s" % node.id, 0, 0) + else: + is_neighbor = 0 + if node.neighbor: + is_neighbor = 1 + self.container.adapter.node_updated("R%s" % node.id, 1, is_neighbor) + + +class RemoteNode(object): + + def __init__(self, node_id): + self.id = node_id + self.neighbor = None + self.remote = None + + def set_neighbor(self): + self.neighbor = True + + def set_remote(self): + self.remote = True + + def clear_neighbor(self): + self.neighbor = None + + def clear_remote(self): + self.remote = None + + def to_delete(self): + return self.neighbor or self.remote + diff --git a/qpid/extras/dispatch/python/qpid/dispatch/router/path.py b/qpid/extras/dispatch/python/qpid/dispatch/router/path.py new file mode 100644 index 0000000000..c051dbe7fc --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/router/path.py @@ -0,0 +1,202 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +try: + from dispatch import * +except ImportError: + from ..stubs import * + +class PathEngine(object): + """ + This module is responsible for computing the next-hop for every router/area in the domain + based on the collection of link states that have been gathered. + """ + def __init__(self, container): + self.container = container + self.id = self.container.id + self.area = self.container.area + self.recalculate = False + self.collection = None + + + def tick(self, now_unused): + if self.recalculate: + self.recalculate = False + self._calculate_routes() + + + def ls_collection_changed(self, collection): + self.recalculate = True + self.collection = collection + + + def _calculate_tree_from_root(self, root): + ## + ## Make a copy of the current collection of link-states that contains + ## an empty link-state for nodes that are known-peers but are not in the + ## collection currently. This is needed to establish routes to those nodes + ## so we can trade link-state information with them. + ## + link_states = {} + for _id, ls in self.collection.items(): + link_states[_id] = ls.peers + for p in ls.peers: + if p not in link_states: + link_states[p] = [] + + ## + ## Setup Dijkstra's Algorithm + ## + cost = {} + prev = {} + for _id in link_states: + cost[_id] = None # infinite + prev[_id] = None # undefined + cost[root] = 0 # no cost to the root node + unresolved = NodeSet(cost) + + ## + ## Process unresolved nodes until lowest cost paths to all reachable nodes have been found. + ## + while not unresolved.empty(): + u = unresolved.lowest_cost() + if cost[u] == None: + # There are no more reachable nodes in unresolved + break + for v in link_states[u]: + if unresolved.contains(v): + alt = cost[u] + 1 # TODO - Use link cost instead of 1 + if cost[v] == None or alt < cost[v]: + cost[v] = alt + prev[v] = u + unresolved.set_cost(v, alt) + + ## + ## Remove unreachable nodes from the map. Note that this will also remove the + ## root node (has no previous node) from the map. + ## + for u, val in prev.items(): + if not val: + prev.pop(u) + + ## + ## Return previous-node map. This is a map of all reachable, remote nodes to + ## their predecessor node. + ## + return prev + + + def _calculate_routes(self): + ## + ## Generate the shortest-path tree with the local node as root + ## + prev = self._calculate_tree_from_root(self.id) + nodes = prev.keys() + + ## + ## Distill the path tree into a map of next hops for each node + ## + next_hops = {} + while len(nodes) > 0: + u = nodes[0] # pick any destination + path = [u] + nodes.remove(u) + v = prev[u] + while v != self.id: # build a list of nodes in the path back to the root + if v in nodes: + path.append(v) + nodes.remove(v) + u = v + v = prev[u] + for w in path: # mark each node in the path as reachable via the next hop + next_hops[w] = u + + ## + ## TODO - Calculate the tree from each origin, determine the set of origins-per-dest + ## for which the path from origin to dest passes through us. This is the set + ## of valid origins for forwarding to the destination. + ## + + self.container.next_hops_changed(next_hops) + + +class NodeSet(object): + """ + This data structure is an ordered list of node IDs, sorted in increasing order by their cost. + Equal cost nodes are secondarily sorted by their ID in order to provide deterministic and + repeatable ordering. + """ + def __init__(self, cost_map): + self.nodes = [] + for _id, cost in cost_map.items(): + ## + ## Assume that nodes are either unreachable (cost = None) or local (cost = 0) + ## during this initialization. + ## + if cost == 0: + self.nodes.insert(0, (_id, cost)) + else: + ## + ## There is no need to sort unreachable nodes by ID + ## + self.nodes.append((_id, cost)) + + + def __repr__(self): + return self.nodes.__repr__() + + + def empty(self): + return len(self.nodes) == 0 + + + def contains(self, _id): + for a, b in self.nodes: + if a == _id: + return True + return False + + + def lowest_cost(self): + """ + Remove and return the lowest cost node ID. + """ + _id, cost = self.nodes.pop(0) + return _id + + + def set_cost(self, _id, new_cost): + """ + Set the cost for an ID in the NodeSet and re-insert the ID so that the list + remains sorted in increasing cost order. + """ + index = 0 + for i, c in self.nodes: + if i == _id: + break + index += 1 + self.nodes.pop(index) + + index = 0 + for i, c in self.nodes: + if c == None or new_cost < c or (new_cost == c and _id < i): + break + index += 1 + + self.nodes.insert(index, (_id, new_cost)) diff --git a/qpid/extras/dispatch/python/qpid/dispatch/router/router_engine.py b/qpid/extras/dispatch/python/qpid/dispatch/router/router_engine.py new file mode 100644 index 0000000000..18b48379c5 --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/router/router_engine.py @@ -0,0 +1,280 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +from time import time +from uuid import uuid4 + +from configuration import Configuration +from data import * +from neighbor import NeighborEngine +from link import LinkStateEngine +from path import PathEngine +from mobile import MobileAddressEngine +from routing import RoutingTableEngine +from binding import BindingEngine +from adapter import AdapterEngine +from node import NodeTracker + +## +## Import the Dispatch adapters from the environment. If they are not found +## (i.e. we are in a test bench, etc.), load the stub versions. +## +try: + from dispatch import * +except ImportError: + from ..stubs import * + + +class RouterEngine: + """ + """ + + def __init__(self, router_adapter, router_id=None, area='area', config_override={}): + """ + Initialize an instance of a router for a domain. + """ + ## + ## Record important information about this router instance + ## + self.domain = "domain" + self.router_adapter = router_adapter + self.log_adapter = LogAdapter("dispatch.router") + self.io_adapter = IoAdapter(self, "qdxrouter") + + if router_id: + self.id = router_id + else: + self.id = str(uuid4()) + self.area = area + self.log(LOG_INFO, "Router Engine Instantiated: area=%s id=%s" % (self.area, self.id)) + + ## + ## Setup configuration + ## + self.config = Configuration(config_override) + self.log(LOG_INFO, "Config: %r" % self.config) + + ## + ## Launch the sub-module engines + ## + self.neighbor_engine = NeighborEngine(self) + self.link_state_engine = LinkStateEngine(self) + self.path_engine = PathEngine(self) + self.mobile_address_engine = MobileAddressEngine(self) + self.routing_table_engine = RoutingTableEngine(self) + self.binding_engine = BindingEngine(self) + self.adapter_engine = AdapterEngine(self) + self.node_tracker = NodeTracker(self) + + + + ##======================================================================================== + ## Adapter Entry Points - invoked from the adapter + ##======================================================================================== + def getId(self): + """ + Return the router's ID + """ + return self.id + + + def addLocalAddress(self, key): + """ + """ + try: + if key.find('_topo') == 0 or key.find('_local') == 0: + return + self.mobile_address_engine.add_local_address(key) + except Exception, e: + self.log(LOG_ERROR, "Exception in new-address processing: exception=%r" % e) + + def delLocalAddress(self, key): + """ + """ + try: + if key.find('_topo') == 0 or key.find('_local') == 0: + return + self.mobile_address_engine.del_local_address(key) + except Exception, e: + self.log(LOG_ERROR, "Exception in del-address processing: exception=%r" % e) + + + def handleTimerTick(self): + """ + """ + try: + now = time() + self.neighbor_engine.tick(now) + self.link_state_engine.tick(now) + self.path_engine.tick(now) + self.mobile_address_engine.tick(now) + self.routing_table_engine.tick(now) + self.binding_engine.tick(now) + self.adapter_engine.tick(now) + self.node_tracker.tick(now) + except Exception, e: + self.log(LOG_ERROR, "Exception in timer processing: exception=%r" % e) + + + def handleControlMessage(self, opcode, body): + """ + """ + try: + now = time() + if opcode == 'HELLO': + msg = MessageHELLO(body) + self.log(LOG_TRACE, "RCVD: %r" % msg) + self.neighbor_engine.handle_hello(msg, now) + + elif opcode == 'RA': + msg = MessageRA(body) + self.log(LOG_TRACE, "RCVD: %r" % msg) + self.link_state_engine.handle_ra(msg, now) + self.mobile_address_engine.handle_ra(msg, now) + + elif opcode == 'LSU': + msg = MessageLSU(body) + self.log(LOG_TRACE, "RCVD: %r" % msg) + self.link_state_engine.handle_lsu(msg, now) + + elif opcode == 'LSR': + msg = MessageLSR(body) + self.log(LOG_TRACE, "RCVD: %r" % msg) + self.link_state_engine.handle_lsr(msg, now) + + elif opcode == 'MAU': + msg = MessageMAU(body) + self.log(LOG_TRACE, "RCVD: %r" % msg) + self.mobile_address_engine.handle_mau(msg, now) + + elif opcode == 'MAR': + msg = MessageMAR(body) + self.log(LOG_TRACE, "RCVD: %r" % msg) + self.mobile_address_engine.handle_mar(msg, now) + + except Exception, e: + self.log(LOG_ERROR, "Exception in message processing: opcode=%s body=%r exception=%r" % (opcode, body, e)) + + + def receive(self, message_properties, body): + """ + This is the IoAdapter message-receive handler + """ + try: + self.handleControlMessage(message_properties['opcode'], body) + except Exception, e: + self.log(LOG_ERROR, "Exception in raw message processing: properties=%r body=%r exception=%r" % + (message_properties, body, e)) + + def getRouterData(self, kind): + """ + """ + if kind == 'help': + return { 'help' : "Get list of supported values for kind", + 'link-state' : "This router's link state", + 'link-state-set' : "The set of link states from known routers", + 'next-hops' : "Next hops to each known router", + 'topo-table' : "Topological routing table", + 'mobile-table' : "Mobile key routing table" + } + if kind == 'link-state' : return self.neighbor_engine.link_state.to_dict() + if kind == 'next-hops' : return self.routing_table_engine.next_hops + if kind == 'topo-table' : return {'table': self.adapter_engine.key_classes['topological']} + if kind == 'mobile-table' : return {'table': self.adapter_engine.key_classes['mobile-key']} + if kind == 'link-state-set' : + copy = {} + for _id,_ls in self.link_state_engine.collection.items(): + copy[_id] = _ls.to_dict() + return copy + + return {'notice':'Use kind="help" to get a list of possibilities'} + + + ##======================================================================================== + ## Adapter Calls - outbound calls to Dispatch + ##======================================================================================== + def log(self, level, text): + """ + Emit a log message to the host's event log + """ + self.log_adapter.log(level, text) + + + def send(self, dest, msg): + """ + Send a control message to another router. + """ + app_props = {'opcode' : msg.get_opcode() } + self.io_adapter.send(dest, app_props, msg.to_dict()) + self.log(LOG_TRACE, "SENT: %r dest=%s" % (msg, dest)) + + + def node_updated(self, addr, reachable, neighbor): + """ + """ + self.router_adapter(addr, reachable, neighbor) + + + ##======================================================================================== + ## Interconnect between the Sub-Modules + ##======================================================================================== + def local_link_state_changed(self, link_state): + self.log(LOG_DEBUG, "Event: local_link_state_changed: %r" % link_state) + self.link_state_engine.new_local_link_state(link_state) + + def ls_collection_changed(self, collection): + self.log(LOG_DEBUG, "Event: ls_collection_changed: %r" % collection) + self.path_engine.ls_collection_changed(collection) + + def next_hops_changed(self, next_hop_table): + self.log(LOG_DEBUG, "Event: next_hops_changed: %r" % next_hop_table) + self.routing_table_engine.next_hops_changed(next_hop_table) + self.binding_engine.next_hops_changed() + + def mobile_sequence_changed(self, mobile_seq): + self.log(LOG_DEBUG, "Event: mobile_sequence_changed: %d" % mobile_seq) + self.link_state_engine.set_mobile_sequence(mobile_seq) + + def mobile_keys_changed(self, keys): + self.log(LOG_DEBUG, "Event: mobile_keys_changed: %r" % keys) + self.binding_engine.mobile_keys_changed(keys) + + def get_next_hops(self): + return self.routing_table_engine.get_next_hops() + + def remote_routes_changed(self, key_class, routes): + self.log(LOG_DEBUG, "Event: remote_routes_changed: class=%s routes=%r" % (key_class, routes)) + self.adapter_engine.remote_routes_changed(key_class, routes) + + def new_neighbor(self, rid): + self.log(LOG_DEBUG, "Event: new_neighbor: id=%s" % rid) + self.node_tracker.new_neighbor(rid) + + def lost_neighbor(self, rid): + self.log(LOG_DEBUG, "Event: lost_neighbor: id=%s" % rid) + self.node_tracker.lost_neighbor(rid) + + def new_node(self, rid): + self.log(LOG_DEBUG, "Event: new_node: id=%s" % rid) + self.node_tracker.new_node(rid) + + def lost_node(self, rid): + self.log(LOG_DEBUG, "Event: lost_node: id=%s" % rid) + self.node_tracker.lost_node(rid) + diff --git a/qpid/extras/dispatch/python/qpid/dispatch/router/routing.py b/qpid/extras/dispatch/python/qpid/dispatch/router/routing.py new file mode 100644 index 0000000000..8030582177 --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/router/routing.py @@ -0,0 +1,56 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +try: + from dispatch import * +except ImportError: + from ..stubs import * + +class RoutingTableEngine(object): + """ + This module is responsible for converting the set of next hops to remote routers to a routing + table in the "topological" address class. + """ + def __init__(self, container): + self.container = container + self.id = self.container.id + self.area = self.container.area + self.next_hops = {} + + + def tick(self, now): + pass + + + def next_hops_changed(self, next_hops): + # Convert next_hops into routing table + self.next_hops = next_hops + new_table = [] + for _id, next_hop in next_hops.items(): + new_table.append(('_topo.%s.%s.#' % (self.area, _id), next_hop)) + pair = ('_topo.%s.all' % (self.area), next_hop) + if new_table.count(pair) == 0: + new_table.append(pair) + + self.container.remote_routes_changed('topological', new_table) + + + def get_next_hops(self): + return self.next_hops + diff --git a/qpid/extras/dispatch/python/qpid/dispatch/stubs/__init__.py b/qpid/extras/dispatch/python/qpid/dispatch/stubs/__init__.py new file mode 100644 index 0000000000..85100d26df --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/stubs/__init__.py @@ -0,0 +1,22 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +from .logadapter import * +from .ioadapter import * + diff --git a/qpid/extras/dispatch/python/qpid/dispatch/stubs/ioadapter.py b/qpid/extras/dispatch/python/qpid/dispatch/stubs/ioadapter.py new file mode 100644 index 0000000000..1e465f98c3 --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/stubs/ioadapter.py @@ -0,0 +1,27 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +class IoAdapter: + def __init__(self, handler, address): + self.handler = handler + self.address = address + + def send(self, address, app_properties, body): + print "IO: send(addr=%s props=%r body=%r" % (address, app_properties, body) + diff --git a/qpid/extras/dispatch/python/qpid/dispatch/stubs/logadapter.py b/qpid/extras/dispatch/python/qpid/dispatch/stubs/logadapter.py new file mode 100644 index 0000000000..3d717d3ed2 --- /dev/null +++ b/qpid/extras/dispatch/python/qpid/dispatch/stubs/logadapter.py @@ -0,0 +1,33 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +LOG_TRACE = 1 +LOG_DEBUG = 2 +LOG_INFO = 4 +LOG_NOTICE = 8 +LOG_WARNING = 16 +LOG_ERROR = 32 +LOG_CRITICAL = 64 + +class LogAdapter: + def __init__(self, mod_name): + self.mod_name = mod_name + + def log(self, level, text): + print "LOG: mod=%s level=%d text=%s" % (self.mod_name, level, text) |
