diff options
-rw-r--r-- | doc/roadmap.rst | 6 | ||||
-rw-r--r-- | ncserver/base/modman.py | 131 |
2 files changed, 117 insertions, 20 deletions
diff --git a/doc/roadmap.rst b/doc/roadmap.rst index 0a752ed..19a1b2f 100644 --- a/doc/roadmap.rst +++ b/doc/roadmap.rst @@ -6,7 +6,7 @@ 1.0 (2020 Q3) ============= -[/] Module management system +[X] Module management system ---------------------------- * [X] Track name/revision/namespace, and register with NSMAP @@ -14,9 +14,9 @@ * Namespace includes prefix, since this is standardised. i.e. ns "urn:ietf:params:xml:ns:yang:ietf-interfaces" -> prefix "if:". -* [ ] Track features implemented +* [X] Track features implemented -* [ ] Handle import-only modules +* [X] Handle import-only modules * [ ] Submodule support? (possibly post-1.0) diff --git a/ncserver/base/modman.py b/ncserver/base/modman.py index 551c1cd..1423be1 100644 --- a/ncserver/base/modman.py +++ b/ncserver/base/modman.py @@ -24,6 +24,75 @@ MODULE_SET_CHANGE_SIGNAL = Signal(('base/ModuleManager', 'module_set_changed')) yang-library-change notification should be fired, when this is supported.""" +def _import_compatible(existing, improps): + """Determine if an imported module is compatible with the existing + module that has the same name/revision. + + :param existing: + The dictionary of properties for the existing module. + + :param improps: + The dictionary of properties for the candidate module. + + :returns bool: + True if compatible, False otherwise. + """ + return (improps.keys() == existing.keys() and + all([existing[key] == improps[key] for key in existing.keys()])) + + +def _gen_modnode(parent, module): + """Generate a yanglib:module node. + + The generated node is in the NMDA format. With some massaging, it + could be used for the deprecated non-NMDA format. + + :param parent: + The parent XML node. + + :param module: + The module. + + :returns: + The generated module node. + """ + modnode = util.subelm(parent, 'yanglib:module') + modnode.append(util.leaf_elm('yanglib:name', module.M_NAME)) + modnode.append(util.leaf_elm('yanglib:revision', module.M_REVISION)) + modnode.append(util.leaf_elm('yanglib:namespace', module.M_NS)) + if getattr(module, 'M_FEATURES', None) is not None: + for feature in module.M_FEATURES: + modnode.append(util.leaf_elm('yanglib:feature', feature)) + return modnode + + +def _gen_modnode_import(parent, nodename, imname, improps): + """Generate a node for an import-only module. + + :param parent: + The parent XML node. + + :param nodename: + The name of the XML node to create. For NMDA library contents, use + 'yanglib:import-only-module'; otherwise, use 'yanglib:module'. + + :param imname: + The name of the imported module in M_IMPORTS syntax. + + :param improps: + The M_IMPORTS property dictionary for this imported module. + + :returns: + The generated module node. + """ + imn_name, imn_rev = imname.split('@') + modnode = util.subelm(parent, nodename) + modnode.append(util.leaf_elm('yanglib:name', imn_name)) + modnode.append(util.leaf_elm('yanglib:revision', imn_rev)) + modnode.append(util.leaf_elm('yanglib:namespace', improps['ns'])) + return modnode + + class ModuleManager: """Manages the modules for the NETCONF for APK Distributions server. @@ -39,9 +108,16 @@ class ModuleManager: M_NS = "urn:ietf:params:xml:ns:yang:ietf-yang-library" M_NAME = "ietf-yang-library" M_REVISION = "2019-01-04" + M_IMPORTS = {'ietf-yang-types@2013-07-15': + {'ns': "urn:ietf:params:xml:ns:yang:ietf-yang-types", + 'prefix': "yang"}, + 'ietf-inet-types@2013-07-16': + {'ns': "urn:ietf:params:xml:ns:yang:ietf-inet-types", + 'prefix': "inet"}} def __init__(self): self.modules = {'ncserver.base.modman': self} + self.imports = dict(self.M_IMPORTS) self.library = {'ncserver.base.modman': '{}@{}'.format(self.M_NAME, self.M_REVISION)} nsmap_add(self.M_PREFIX, self.M_NS) @@ -80,34 +156,47 @@ class ModuleManager: mod.M_ABI_VERSION) return None + if getattr(mod, 'M_IMPORTS', None) is not None: + for imname, improps in mod.M_IMPORTS: + if imname in self.imports: + if not _import_compatible(self.imports[imname], improps): + self.logger.error(_("Module '%s' has incompatible " + "imported module '%s'"), name, + imname) + return None + else: + self.imports[imname] = improps + self.logger.info(_("Loading module '%s' with ABI %d for namespace %s"), name, mod.M_ABI_VERSION, mod.M_NS) self.modules[name] = mod self.library[name] = '{}@{}'.format(mod.M_NAME, mod.M_REVISION) nsmap_add(mod.M_PREFIX, mod.M_NS) + MODULE_SET_CHANGE_SIGNAL.call(name, mod) return name def running(self, node): """Return running configuration information.""" # ietf-yang-library is state-only. - def operational(self, node): - """Return configuration and device state information.""" - module_set_id = hashlib.sha256(','.join(self.modules.keys()).encode()) - content_id = module_set_id.hexdigest() + def _gen_yang_library(self, node, content_id): + """Generate yanglib:yang-library node. - # NDMA-enabled yang-library node. + :param node: + The XML node to append to. + + :param content_id: + The unique Content ID for this library. + """ lib = util.subelm(node, 'yanglib:yang-library') modset = util.subelm(lib, 'yanglib:module-set') modset.append(util.leaf_elm('yanglib:name', 'netconfapk')) for module in self.modules.values(): - # XXX or import-only-module, if it's import-only - modnode = util.subelm(modset, 'yanglib:module') - modnode.append(util.leaf_elm('yanglib:name', module.M_NAME)) - modnode.append(util.leaf_elm('yanglib:revision', - module.M_REVISION)) - modnode.append(util.leaf_elm('yanglib:namespace', module.M_NS)) + _gen_modnode(modset, module) + for imname, improps in self.imports.items(): + _gen_modnode_import(modset, 'yanglib:import-only-module', imname, + improps) schema = util.subelm(lib, 'yanglib:schema') schema.append(util.leaf_elm('yanglib:name', 'apkschema')) schema.append(util.leaf_elm('yanglib:module-set', 'netconfapk')) @@ -118,19 +207,27 @@ class ModuleManager: dsnode.append(util.leaf_elm('schema', 'apkschema')) lib.append(util.leaf_elm('yanglib:content-id', content_id)) + return lib + + def operational(self, node): + """Return configuration and device state information.""" + module_set_id = hashlib.sha256(','.join(self.modules.keys()).encode()) + content_id = module_set_id.hexdigest() + + # NDMA-enabled yang-library node. + self._gen_yang_library(node, content_id) # Deprecated modules-state node. modstate = util.subelm(node, 'yanglib:modules-state') modstate.append(util.leaf_elm('yanglib:module-set-id', content_id)) for module in self.modules.values(): - modnode = util.subelm(modstate, 'yanglib:module') - modnode.append(util.leaf_elm('yanglib:name', module.M_NAME)) - modnode.append(util.leaf_elm('yanglib:revision', - module.M_REVISION)) - modnode.append(util.leaf_elm('yanglib:namespace', module.M_NS)) - # XXX import-only module support: use 'import' + modnode = _gen_modnode(modstate, module) modnode.append(util.leaf_elm('yanglib:conformance-type', 'implement')) + for imname, improps in self.imports.items(): + modnode = _gen_modnode_import(modstate, 'yanglib:module', imname, + improps) + modnode.append(util.leaf_elm('yanglib:conformance-type', 'import')) def collect_running(self, node): """Collect all available information for the <running> datastore.""" |