From 965abdca5f308fe00af341d582663a29d5fd89f9 Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Thu, 17 Oct 2024 18:59:16 +0100 Subject: [PATCH] work to facilitate generic json command --- src/exabgp/application/cli.py | 31 ++- src/exabgp/application/validate.py | 3 +- src/exabgp/bgp/neighbor.py | 179 +--------------- src/exabgp/reactor/api/__init__.py | 22 +- src/exabgp/reactor/api/command/announce.py | 224 +++++++++---------- src/exabgp/reactor/api/command/command.py | 6 +- src/exabgp/reactor/api/command/neighbor.py | 237 ++++++++++++++++++--- src/exabgp/reactor/api/command/reactor.py | 47 ++-- src/exabgp/reactor/api/command/rib.py | 128 ++++++----- src/exabgp/reactor/api/command/watchdog.py | 12 +- src/exabgp/reactor/api/processes.py | 14 +- src/exabgp/reactor/api/response/answer.py | 13 +- src/exabgp/reactor/loop.py | 2 +- 13 files changed, 490 insertions(+), 428 deletions(-) diff --git a/src/exabgp/application/cli.py b/src/exabgp/application/cli.py index c9cd9a291..9131ce34d 100644 --- a/src/exabgp/application/cli.py +++ b/src/exabgp/application/cli.py @@ -38,9 +38,12 @@ class AnswerStream: - done = '\n%s\n' % Answer.done - error = '\n%s\n' % Answer.error - shutdown = '\n%s\n' % Answer.error + text_done = '\n%s\n' % Answer.text_done + text_error = '\n%s\n' % Answer.text_error + text_shutdown = '\n%s\n' % Answer.text_error + json_done = '\n%s\n' % Answer.json_done + json_error = '\n%s\n' % Answer.json_error + json_shutdown = '\n%s\n' % Answer.json_error buffer_size = Answer.buffer_size + 2 @@ -174,13 +177,21 @@ def cmdline(cmdarg): break # we read some data but it is not ending by a new line (ie: not a command completion) - if rbuffer[-1] != 10: # \n + if rbuffer[-1] != ord('\n'): continue - if AnswerStream.done.endswith(rbuffer.decode()[-len(AnswerStream.done) :]): + + if AnswerStream.done.endswith(rbuffer.decode()[-len(AnswerStream.text_done) :]): + break + if AnswerStream.error.endswith(rbuffer.decode()[-len(AnswerStream.text_error) :]): + break + if AnswerStream.shutdown.endswith(rbuffer.decode()[-len(AnswerStream.text_shutdown) :]): + break + + if AnswerStream.done.endswith(rbuffer.decode()[-len(AnswerStream.json_done) :]): break - if AnswerStream.error.endswith(rbuffer.decode()[-len(AnswerStream.error) :]): + if AnswerStream.error.endswith(rbuffer.decode()[-len(AnswerStream.json_error) :]): break - if AnswerStream.shutdown.endswith(rbuffer.decode()[-len(AnswerStream.shutdown) :]): + if AnswerStream.shutdown.endswith(rbuffer.decode()[-len(AnswerStream.json_shutdown) :]): break renamed = [''] @@ -293,15 +304,15 @@ def cmdline(cmdarg): while b'\n' in buf: line, buf = buf.split(b'\n', 1) string = line.decode() - if string == Answer.done: + if string == Answer.text_done or string == Answer.json_done: done = True break - if string == Answer.shutdown: + if string == Answer.text_shutdown or string == Answer.json_shutdown: sys.stderr.write('ExaBGP is shutting down, command aborted\n') sys.stderr.flush() done = True break - if string == Answer.error: + if string == Answer.text_error or string == Answer.json_error: done = True sys.stderr.write('ExaBGP returns an error (see ExaBGP\'s logs for more information)\n') sys.stderr.write('use help for a list of available commands\n') diff --git a/src/exabgp/application/validate.py b/src/exabgp/application/validate.py index c9da274d0..3e128ef1a 100644 --- a/src/exabgp/application/validate.py +++ b/src/exabgp/application/validate.py @@ -10,6 +10,7 @@ from exabgp.environment import getconf from exabgp.configuration.configuration import Configuration +from exabgp.reactor.api.command.neighbor import NeighborTemplate from exabgp.debug import trace_interceptor from exabgp.logger import log @@ -61,7 +62,7 @@ def cmdline(cmdarg): if cmdarg.neighbor: log.warning('checking neighbors', 'configuration') for name, neighbor in config.neighbors.items(): - reparsed = neighbor.string() + reparsed = NeighborTemplate.configuration(neighbor) log.debug(reparsed, configuration) log.info(f'\u2713 neighbor {name.split()[1]}', 'configuration') diff --git a/src/exabgp/bgp/neighbor.py b/src/exabgp/bgp/neighbor.py index 429ccb9ad..70719e6d2 100644 --- a/src/exabgp/bgp/neighbor.py +++ b/src/exabgp/bgp/neighbor.py @@ -25,6 +25,7 @@ from exabgp.rib import RIB +from exabgp.reactor.api.command.neighbor import NeighborTemplate # class Section(dict): # name = '' @@ -290,182 +291,6 @@ def __eq__(self, other): def __ne__(self, other): return not self.__eq__(other) - def string(self, with_changes=True): - changes = '' - if with_changes: - changes += '\nstatic { ' - for change in self.rib.outgoing.queued_changes(): - changes += '\n\t\t%s' % change.extensive() - changes += '\n}' - - families = '' - for afi, safi in self.families(): - families += '\n\t\t%s %s;' % (afi.name(), safi.name()) - - nexthops = '' - for afi, safi, nexthop in self.nexthops(): - nexthops += '\n\t\t%s %s %s;' % (afi.name(), safi.name(), nexthop.name()) - - addpaths = '' - for afi, safi in self.addpaths(): - addpaths += '\n\t\t%s %s;' % (afi.name(), safi.name()) - - codes = Message.CODE - - _extension_global = { - 'neighbor-changes': 'neighbor-changes', - 'negotiated': 'negotiated', - 'fsm': 'fsm', - 'signal': 'signal', - } - - _extension_receive = { - 'receive-packets': 'packets', - 'receive-parsed': 'parsed', - 'receive-consolidate': 'consolidate', - 'receive-%s' % codes.NOTIFICATION.SHORT: 'notification', - 'receive-%s' % codes.OPEN.SHORT: 'open', - 'receive-%s' % codes.KEEPALIVE.SHORT: 'keepalive', - 'receive-%s' % codes.UPDATE.SHORT: 'update', - 'receive-%s' % codes.ROUTE_REFRESH.SHORT: 'refresh', - 'receive-%s' % codes.OPERATIONAL.SHORT: 'operational', - } - - _extension_send = { - 'send-packets': 'packets', - 'send-parsed': 'parsed', - 'send-consolidate': 'consolidate', - 'send-%s' % codes.NOTIFICATION.SHORT: 'notification', - 'send-%s' % codes.OPEN.SHORT: 'open', - 'send-%s' % codes.KEEPALIVE.SHORT: 'keepalive', - 'send-%s' % codes.UPDATE.SHORT: 'update', - 'send-%s' % codes.ROUTE_REFRESH.SHORT: 'refresh', - 'send-%s' % codes.OPERATIONAL.SHORT: 'operational', - } - - apis = '' - - for process in self.api.get('processes', []): - _global = [] - _receive = [] - _send = [] - - for api, name in _extension_global.items(): - _global.extend( - [ - '\t\t%s;\n' % name, - ] - if process in self.api[api] - else [] - ) - - for api, name in _extension_receive.items(): - _receive.extend( - [ - '\t\t\t%s;\n' % name, - ] - if process in self.api[api] - else [] - ) - - for api, name in _extension_send.items(): - _send.extend( - [ - '\t\t\t%s;\n' % name, - ] - if process in self.api[api] - else [] - ) - - _api = '\tapi {\n' - _api += '\t\tprocesses [ %s ];\n' % process - _api += ''.join(_global) - if _receive: - _api += '\t\treceive {\n' - _api += ''.join(_receive) - _api += '\t\t}\n' - if _send: - _api += '\t\tsend {\n' - _api += ''.join(_send) - _api += '\t\t}\n' - _api += '\t}\n' - - apis += _api - - returned = ( - 'neighbor %s {\n' - '\tdescription "%s";\n' - '\trouter-id %s;\n' - '\thost-name %s;\n' - '\tdomain-name %s;\n' - '\tlocal-address %s;\n' - '\tsource-interface %s;\n' - '\tlocal-as %s;\n' - '\tpeer-as %s;\n' - '\thold-time %s;\n' - '\trate-limit %s;\n' - '\tmanual-eor %s;\n' - '%s%s%s%s%s%s%s%s%s%s%s\n' - '\tcapability {\n' - '%s%s%s%s%s%s%s%s%s%s\t}\n' - '\tfamily {%s\n' - '\t}\n' - '\tnexthop {%s\n' - '\t}\n' - '\tadd-path {%s\n' - '\t}\n' - '%s' - '%s' - '}' - % ( - self['peer-address'], - self['description'], - self['router-id'], - self['host-name'], - self['domain-name'], - self['local-address'] if not self.auto_discovery else 'auto', - self['source-interface'], - self['local-as'], - self['peer-as'], - self['hold-time'], - 'disable' if self['rate-limit'] == 0 else self['rate-limit'], - 'true' if self['manual-eor'] else 'false', - '\n\tpassive %s;\n' % ('true' if self['passive'] else 'false'), - '\n\tlisten %d;\n' % self['listen'] if self['listen'] else '', - '\n\tconnect %d;\n' % self['connect'] if self['connect'] else '', - '\tgroup-updates %s;\n' % ('true' if self['group-updates'] else 'false'), - '\tauto-flush %s;\n' % ('true' if self['auto-flush'] else 'false'), - '\tadj-rib-in %s;\n' % ('true' if self['adj-rib-in'] else 'false'), - '\tadj-rib-out %s;\n' % ('true' if self['adj-rib-out'] else 'false'), - '\tmd5-password "%s";\n' % self['md5-password'] if self['md5-password'] else '', - '\tmd5-base64 %s;\n' - % ('true' if self['md5-base64'] is True else 'false' if self['md5-base64'] is False else 'auto'), - '\tmd5-ip "%s";\n' % self['md5-ip'] if not self.auto_discovery else '', - '\toutgoing-ttl %s;\n' % self['outgoing-ttl'] if self['outgoing-ttl'] else '', - '\tincoming-ttl %s;\n' % self['incoming-ttl'] if self['incoming-ttl'] else '', - '\t\tasn4 %s;\n' % ('enable' if self['capability']['asn4'] else 'disable'), - '\t\troute-refresh %s;\n' % ('enable' if self['capability']['route-refresh'] else 'disable'), - '\t\tgraceful-restart %s;\n' - % (self['capability']['graceful-restart'] if self['capability']['graceful-restart'] else 'disable'), - '\t\tsoftware-version %s;\n' % ('enable' if self['capability']['software-version'] else 'disable'), - '\t\tnexthop %s;\n' % ('enable' if self['capability']['nexthop'] else 'disable'), - '\t\tadd-path %s;\n' - % (AddPath.string[self['capability']['add-path']] if self['capability']['add-path'] else 'disable'), - '\t\tmulti-session %s;\n' % ('enable' if self['capability']['multi-session'] else 'disable'), - '\t\toperational %s;\n' % ('enable' if self['capability']['operational'] else 'disable'), - '\t\taigp %s;\n' % ('enable' if self['capability']['aigp'] else 'disable'), - families, - nexthops, - addpaths, - apis, - changes, - ) - ) - - # '\t\treceive {\n%s\t\t}\n' % receive if receive else '', - # '\t\tsend {\n%s\t\t}\n' % send if send else '', - return returned.replace('\t', ' ') - def ip_self(self, afi): if afi == self['local-address'].afi: return self['local-address'] @@ -490,4 +315,4 @@ def remove_self(self, changes): return change def __str__(self): - return self.string(False) + return NeighborTemplate.configuration(self, False) diff --git a/src/exabgp/reactor/api/__init__.py b/src/exabgp/reactor/api/__init__.py index ce80b8f17..a9e5c8c60 100644 --- a/src/exabgp/reactor/api/__init__.py +++ b/src/exabgp/reactor/api/__init__.py @@ -37,11 +37,29 @@ def log_failure(self, message, level='ERR'): report = '%s\nreason: %s' % (message, error) if error else message log.error(report, 'processes', level) + def process(self, reactor, service, command): + # it to allow a global "set encoding json" + # it to allow a global "set encoding text" + # to not have to set the encoding on each command + if 'json' in command.split(' '): + return self.json(reactor, service, command) + if 'text' in command.split(' '): + return self.text(reactor, service, command) + return self.text(reactor, service, command) + def text(self, reactor, service, command): for registered in self.functions: if registered == command or command.endswith(' ' + registered) or registered + ' ' in command: - return self.callback['text'][registered](self, reactor, service, command) - reactor.processes.answer_error(service) + return self.callback['text'][registered](self, reactor, service, command, False) + reactor.processes.answer_text_error(service) + log.warning('command from process not understood : %s' % command, 'api') + return False + + def json(self, reactor, service, command): + for registered in self.functions: + if registered == command or command.endswith(' ' + registered) or registered + ' ' in command: + return self.callback['json'][registered](self, reactor, service, command, True) + reactor.processes.answer_json_error(service) log.warning('command from process not understood : %s' % command, 'api') return False diff --git a/src/exabgp/reactor/api/command/announce.py b/src/exabgp/reactor/api/command/announce.py index 7a4b67dc9..8ede7e85a 100644 --- a/src/exabgp/reactor/api/command/announce.py +++ b/src/exabgp/reactor/api/command/announce.py @@ -21,25 +21,25 @@ def register_announce(): pass -# @Command.register('text', 'debug') +# @Command.register('debug') # the command debug is hardcoded in the process code -@Command.register('text', 'announce route') -def announce_route(self, reactor, service, line): +@Command.register('announce route') +def announce_route(self, reactor, service, line, use_json): def callback(): try: descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.peers(service), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return changes = self.api_route(command) if not changes: self.log_failure('command could not parse route in : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -56,36 +56,36 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) except ValueError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True except IndexError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True reactor.asynchronous.schedule(service, line, callback()) return True -@Command.register('text', 'withdraw route') -def withdraw_route(self, reactor, service, line): +@Command.register('withdraw route') +def withdraw_route(self, reactor, service, line, use_json): def callback(): try: descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.peers(service), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return changes = self.api_route(command) if not changes: self.log_failure('command could not parse route in : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -112,36 +112,36 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) except ValueError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True except IndexError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True reactor.asynchronous.schedule(service, line, callback()) return True -@Command.register('text', 'announce vpls') -def announce_vpls(self, reactor, service, line): +@Command.register('announce vpls') +def announce_vpls(self, reactor, service, line, use_json): def callback(): try: descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.peers(service), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return changes = self.api_vpls(command) if not changes: self.log_failure('command could not parse vpls in : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -153,29 +153,29 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) except ValueError: self.log_failure('issue parsing the vpls') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True except IndexError: self.log_failure('issue parsing the vpls') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True reactor.asynchronous.schedule(service, line, callback()) return True -@Command.register('text', 'withdraw vpls') -def withdraw_vpls(self, reactor, service, line): +@Command.register('withdraw vpls') +def withdraw_vpls(self, reactor, service, line, use_json): def callback(): try: descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.peers(service), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -183,7 +183,7 @@ def callback(): if not changes: self.log_failure('command could not parse vpls in : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -200,37 +200,37 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) except ValueError: self.log_failure('issue parsing the vpls') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True except IndexError: self.log_failure('issue parsing the vpls') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True reactor.asynchronous.schedule(service, line, callback()) return True -@Command.register('text', 'announce attribute') -@Command.register('text', 'announce attributes') -def announce_attributes(self, reactor, service, line): +@Command.register('announce attribute') +@Command.register('announce attributes') +def announce_attributes(self, reactor, service, line, use_json): def callback(): try: descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.peers(service), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return changes = self.api_attributes(command, peers) if not changes: self.log_failure('command could not parse route in : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -242,37 +242,37 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) except ValueError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True except IndexError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True reactor.asynchronous.schedule(service, line, callback()) return True -@Command.register('text', 'withdraw attribute') -@Command.register('text', 'withdraw attributes') -def withdraw_attribute(self, reactor, service, line): +@Command.register('withdraw attribute') +@Command.register('withdraw attributes') +def withdraw_attribute(self, reactor, service, line, use_json): def callback(): try: descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.peers(service), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return changes = self.api_attributes(command, peers) if not changes: self.log_failure('command could not parse route in : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -289,36 +289,36 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) except ValueError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True except IndexError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True reactor.asynchronous.schedule(service, line, callback()) return True -@Command.register('text', 'announce flow') -def announce_flow(self, reactor, service, line): +@Command.register('announce flow') +def announce_flow(self, reactor, service, line, use_json): def callback(): try: descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.peers(service), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return changes = self.api_flow(command) if not changes: self.log_failure('command could not parse flow in : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -330,29 +330,29 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) except ValueError: self.log_failure('issue parsing the flow') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True except IndexError: self.log_failure('issue parsing the flow') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True reactor.asynchronous.schedule(service, line, callback()) return True -@Command.register('text', 'withdraw flow') -def withdraw_flow(self, reactor, service, line): +@Command.register('withdraw flow') +def withdraw_flow(self, reactor, service, line, use_json): def callback(): try: descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.peers(service), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -360,7 +360,7 @@ def callback(): if not changes: self.log_failure('command could not parse flow in : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -376,27 +376,27 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) except ValueError: self.log_failure('issue parsing the flow') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True except IndexError: self.log_failure('issue parsing the flow') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True reactor.asynchronous.schedule(service, line, callback()) return True -@Command.register('text', 'announce eor') -def announce_eor(self, reactor, service, command): +@Command.register('announce eor') +def announce_eor(self, reactor, service, line, use_json): def callback(self, command, peers): family = self.api_eor(command) if not family: self.log_failure("Command could not parse eor : %s" % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -407,34 +407,34 @@ def callback(self, command, peers): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) try: - descriptions, command = extract_neighbors(command) + descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.established_peers(), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False reactor.asynchronous.schedule(service, command, callback(self, command, peers)) return True except ValueError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False except IndexError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False -@Command.register('text', 'announce route-refresh') -def announce_refresh(self, reactor, service, command): +@Command.register('announce route-refresh') +def announce_refresh(self, reactor, service, line, use_json): def callback(self, command, peers): refreshes = self.api_refresh(command) if not refreshes: self.log_failure("Command could not parse route-refresh command : %s" % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -446,34 +446,34 @@ def callback(self, command, peers): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) try: - descriptions, command = extract_neighbors(command) + descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.established_peers(), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False reactor.asynchronous.schedule(service, command, callback(self, command, peers)) return True except ValueError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False except IndexError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False -@Command.register('text', 'announce operational') -def announce_operational(self, reactor, service, command): +@Command.register('announce operational') +def announce_operational(self, reactor, service, line, use_json): def callback(self, command, peers): operational = self.api_operational(command) if not operational: self.log_failure("Command could not parse operational command : %s" % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -483,7 +483,7 @@ def callback(self, command, peers): % (', '.join(peers if peers else []) if peers is not None else 'all peers', operational.extensive()) ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) if (command.split() + ['be', 'safe'])[2].lower() not in ( 'asm', @@ -495,44 +495,44 @@ def callback(self, command, peers): 'lpcq', 'lpcp', ): - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) return False try: - descriptions, command = extract_neighbors(command) + descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.peers(service), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False reactor.asynchronous.schedule(service, command, callback(self, command, peers)) return True except ValueError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False except IndexError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False -@Command.register('text', 'announce ipv4') -def announce_ipv4(self, reactor, service, line): +@Command.register('announce ipv4') +def announce_ipv4(self, reactor, service, line, use_json): def callback(): try: descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.peers(service), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return changes = self.api_announce_v4(command) if not changes: self.log_failure('command could not parse ipv4 in : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -544,29 +544,29 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) except ValueError: self.log_failure('issue parsing the ipv4') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True except IndexError: self.log_failure('issue parsing the ipv4') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True reactor.asynchronous.schedule(service, line, callback()) return True -@Command.register('text', 'withdraw ipv4') -def withdraw_ipv4(self, reactor, service, line): +@Command.register('withdraw ipv4') +def withdraw_ipv4(self, reactor, service, line, use_json): def callback(): try: descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.peers(service), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -574,7 +574,7 @@ def callback(): if not changes: self.log_failure('command could not parse ipv4 in : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -590,36 +590,36 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) except ValueError: self.log_failure('issue parsing the ipv4') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True except IndexError: self.log_failure('issue parsing the ipv4') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True reactor.asynchronous.schedule(service, line, callback()) return True -@Command.register('text', 'announce ipv6') -def announce_ipv6(self, reactor, service, line): +@Command.register('announce ipv6') +def announce_ipv6(self, reactor, service, line, use_json): def callback(): try: descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.peers(service), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return changes = self.api_announce_v6(command) if not changes: self.log_failure('command could not parse ipv6 in : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -631,29 +631,29 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) except ValueError: self.log_failure('issue parsing the ipv6') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True except IndexError: self.log_failure('issue parsing the ipv6') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True reactor.asynchronous.schedule(service, line, callback()) return True -@Command.register('text', 'withdraw ipv6') -def withdraw_ipv6(self, reactor, service, line): +@Command.register('withdraw ipv6') +def withdraw_ipv6(self, reactor, service, line, use_json): def callback(): try: descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.peers(service), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -661,7 +661,7 @@ def callback(): if not changes: self.log_failure('command could not parse ipv6 in : %s' % command) - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True return @@ -677,14 +677,14 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) except ValueError: self.log_failure('issue parsing the ipv6') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True except IndexError: self.log_failure('issue parsing the ipv6') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) yield True reactor.asynchronous.schedule(service, line, callback()) diff --git a/src/exabgp/reactor/api/command/command.py b/src/exabgp/reactor/api/command/command.py index b51ab86bc..423209198 100644 --- a/src/exabgp/reactor/api/command/command.py +++ b/src/exabgp/reactor/api/command/command.py @@ -14,7 +14,7 @@ class Command(object): functions = [] @classmethod - def register(cls, encoding, name, neighbor=True, options=None): + def register(cls, name, neighbor=True, options=None, json_support=False): if name not in cls.functions: cls.functions.append(name) cls.functions.sort(reverse=True) @@ -22,7 +22,9 @@ def register(cls, encoding, name, neighbor=True, options=None): def register(function): cls.callback['neighbor'][name] = neighbor - cls.callback[encoding][name] = function + cls.callback['text'][name] = function + if json_support: + cls.callback['json'][name] = function function.func_name = name.replace(' ', '_') return function diff --git a/src/exabgp/reactor/api/command/neighbor.py b/src/exabgp/reactor/api/command/neighbor.py index 162439d4b..34c9a4278 100644 --- a/src/exabgp/reactor/api/command/neighbor.py +++ b/src/exabgp/reactor/api/command/neighbor.py @@ -15,6 +15,9 @@ from exabgp.reactor.api.command.limit import match_neighbor from exabgp.reactor.api.command.limit import extract_neighbors +from exabgp.bgp.message import Message +from exabgp.bgp.message.open.capability import AddPath + def register_neighbor(): pass @@ -42,7 +45,7 @@ def _addpath(send, receive): return "disabled" -class Neighbor(object): +class NeighborTemplate(object): extensive_kv = ' %-20s %15s %15s %15s' extensive_template = """\ Neighbor %(peer-address)s @@ -72,6 +75,184 @@ class Neighbor(object): summary_header = 'Peer AS up/down state | #sent #recvd' summary_template = '%-15s %-7s %9s %-12s %10d %10d' + @classmethod + def configuration(cls, neighbor, with_changes=True): + changes = '' + if with_changes: + changes += '\nstatic { ' + for change in neighbor.rib.outgoing.queued_changes(): + changes += '\n\t\t%s' % change.extensive() + changes += '\n}' + + families = '' + for afi, safi in neighbor.families(): + families += '\n\t\t%s %s;' % (afi.name(), safi.name()) + + nexthops = '' + for afi, safi, nexthop in neighbor.nexthops(): + nexthops += '\n\t\t%s %s %s;' % (afi.name(), safi.name(), nexthop.name()) + + addpaths = '' + for afi, safi in neighbor.addpaths(): + addpaths += '\n\t\t%s %s;' % (afi.name(), safi.name()) + + codes = Message.CODE + + _extension_global = { + 'neighbor-changes': 'neighbor-changes', + 'negotiated': 'negotiated', + 'fsm': 'fsm', + 'signal': 'signal', + } + + _extension_receive = { + 'receive-packets': 'packets', + 'receive-parsed': 'parsed', + 'receive-consolidate': 'consolidate', + 'receive-%s' % codes.NOTIFICATION.SHORT: 'notification', + 'receive-%s' % codes.OPEN.SHORT: 'open', + 'receive-%s' % codes.KEEPALIVE.SHORT: 'keepalive', + 'receive-%s' % codes.UPDATE.SHORT: 'update', + 'receive-%s' % codes.ROUTE_REFRESH.SHORT: 'refresh', + 'receive-%s' % codes.OPERATIONAL.SHORT: 'operational', + } + + _extension_send = { + 'send-packets': 'packets', + 'send-parsed': 'parsed', + 'send-consolidate': 'consolidate', + 'send-%s' % codes.NOTIFICATION.SHORT: 'notification', + 'send-%s' % codes.OPEN.SHORT: 'open', + 'send-%s' % codes.KEEPALIVE.SHORT: 'keepalive', + 'send-%s' % codes.UPDATE.SHORT: 'update', + 'send-%s' % codes.ROUTE_REFRESH.SHORT: 'refresh', + 'send-%s' % codes.OPERATIONAL.SHORT: 'operational', + } + + apis = '' + + for process in neighbor.api.get('processes', []): + _global = [] + _receive = [] + _send = [] + + for api, name in _extension_global.items(): + _global.extend( + [ + '\t\t%s;\n' % name, + ] + if process in neighbor.api[api] + else [] + ) + + for api, name in _extension_receive.items(): + _receive.extend( + [ + '\t\t\t%s;\n' % name, + ] + if process in neighbor.api[api] + else [] + ) + + for api, name in _extension_send.items(): + _send.extend( + [ + '\t\t\t%s;\n' % name, + ] + if process in neighbor.api[api] + else [] + ) + + _api = '\tapi {\n' + _api += '\t\tprocesses [ %s ];\n' % process + _api += ''.join(_global) + if _receive: + _api += '\t\treceive {\n' + _api += ''.join(_receive) + _api += '\t\t}\n' + if _send: + _api += '\t\tsend {\n' + _api += ''.join(_send) + _api += '\t\t}\n' + _api += '\t}\n' + + apis += _api + + returned = ( + 'neighbor %s {\n' + '\tdescription "%s";\n' + '\trouter-id %s;\n' + '\thost-name %s;\n' + '\tdomain-name %s;\n' + '\tlocal-address %s;\n' + '\tsource-interface %s;\n' + '\tlocal-as %s;\n' + '\tpeer-as %s;\n' + '\thold-time %s;\n' + '\trate-limit %s;\n' + '\tmanual-eor %s;\n' + '%s%s%s%s%s%s%s%s%s%s%s\n' + '\tcapability {\n' + '%s%s%s%s%s%s%s%s%s%s\t}\n' + '\tfamily {%s\n' + '\t}\n' + '\tnexthop {%s\n' + '\t}\n' + '\tadd-path {%s\n' + '\t}\n' + '%s' + '%s' + '}' + % ( + neighbor['peer-address'], + neighbor['description'], + neighbor['router-id'], + neighbor['host-name'], + neighbor['domain-name'], + neighbor['local-address'] if not neighbor.auto_discovery else 'auto', + neighbor['source-interface'], + neighbor['local-as'], + neighbor['peer-as'], + neighbor['hold-time'], + 'disable' if neighbor['rate-limit'] == 0 else neighbor['rate-limit'], + 'true' if neighbor['manual-eor'] else 'false', + '\n\tpassive %s;\n' % ('true' if neighbor['passive'] else 'false'), + '\n\tlisten %d;\n' % neighbor['listen'] if neighbor['listen'] else '', + '\n\tconnect %d;\n' % neighbor['connect'] if neighbor['connect'] else '', + '\tgroup-updates %s;\n' % ('true' if neighbor['group-updates'] else 'false'), + '\tauto-flush %s;\n' % ('true' if neighbor['auto-flush'] else 'false'), + '\tadj-rib-in %s;\n' % ('true' if neighbor['adj-rib-in'] else 'false'), + '\tadj-rib-out %s;\n' % ('true' if neighbor['adj-rib-out'] else 'false'), + '\tmd5-password "%s";\n' % neighbor['md5-password'] if neighbor['md5-password'] else '', + '\tmd5-base64 %s;\n' + % ('true' if neighbor['md5-base64'] is True else 'false' if neighbor['md5-base64'] is False else 'auto'), + '\tmd5-ip "%s";\n' % neighbor['md5-ip'] if not neighbor.auto_discovery else '', + '\toutgoing-ttl %s;\n' % neighbor['outgoing-ttl'] if neighbor['outgoing-ttl'] else '', + '\tincoming-ttl %s;\n' % neighbor['incoming-ttl'] if neighbor['incoming-ttl'] else '', + '\t\tasn4 %s;\n' % ('enable' if neighbor['capability']['asn4'] else 'disable'), + '\t\troute-refresh %s;\n' % ('enable' if neighbor['capability']['route-refresh'] else 'disable'), + '\t\tgraceful-restart %s;\n' + % (neighbor['capability']['graceful-restart'] if neighbor['capability']['graceful-restart'] else 'disable'), + '\t\tsoftware-version %s;\n' % ('enable' if neighbor['capability']['software-version'] else 'disable'), + '\t\tnexthop %s;\n' % ('enable' if neighbor['capability']['nexthop'] else 'disable'), + '\t\tadd-path %s;\n' + % (AddPath.string[neighbor['capability']['add-path']] if neighbor['capability']['add-path'] else 'disable'), + '\t\tmulti-session %s;\n' % ('enable' if neighbor['capability']['multi-session'] else 'disable'), + '\t\toperational %s;\n' % ('enable' if neighbor['capability']['operational'] else 'disable'), + '\t\taigp %s;\n' % ('enable' if neighbor['capability']['aigp'] else 'disable'), + families, + nexthops, + addpaths, + apis, + changes, + ) + ) + + # '\t\treceive {\n%s\t\t}\n' % receive if receive else '', + # '\t\tsend {\n%s\t\t}\n' % send if send else '', + return returned.replace('\t', ' ') + + @classmethod def as_dict(cls, answer): up = answer['duration'] @@ -157,6 +338,10 @@ def formated_dict(cls, answer): return formated + @classmethod + def to_json(cls, answer): + return json.dumps(cls.formated_dict(answer)) + @classmethod def extensive(cls, answer): return cls.extensive_template % cls.formated_dict(answer) @@ -173,37 +358,35 @@ def summary(cls, answer): ) -@Command.register('text', 'teardown', True) -def teardown(self, reactor, service, line): +@Command.register('teardown', True) +def teardown(self, reactor, service, line, use_json): try: descriptions, line = extract_neighbors(line) if ' ' not in line: - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False _, code = line.split(' ', 1) if not code.isdigit(): - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False for key in reactor.established_peers(): for description in descriptions: if match_neighbor(description, key): reactor.teardown_peer(key, int(code)) self.log_message('teardown scheduled for %s' % ' '.join(description)) - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) return True except ValueError: - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False except IndexError: - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False -@Command.register('text', 'show neighbor', False, ['summary', 'extensive', 'configuration']) -@Command.register('text', 'show neighbor text', False, ['summary', 'extensive', 'configuration']) -@Command.register('json', 'show neighbor json', False, ['summary', 'extensive', 'configuration']) -def show_neighbor(self, reactor, service, command): - words = command.split() +@Command.register('show neighbor', False, ['summary', 'extensive', 'configuration'], True) +def show_neighbor(self, reactor, service, line, use_json): + words = line.split() extensive = 'extensive' in words configuration = 'configuration' in words @@ -222,8 +405,6 @@ def show_neighbor(self, reactor, service, command): if text: words.remove('text') - use_json = json and not text - limit = words[-1] if words[-1] != 'neighbor' else '' def callback_configuration(): @@ -236,52 +417,52 @@ def callback_configuration(): for line in str(neighbor).split('\n'): reactor.processes.write(service, line) yield True - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) def callback_json(): p = [] for peer_name in reactor.peers(): - p.append(Neighbor.as_dict(reactor.neighbor_cli_data(peer_name))) + p.append(NeighborTemplate.as_dict(reactor.neighbor_cli_data(peer_name))) for line in json.dumps(p).split('\n'): reactor.processes.write(service, line) yield True - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) def callback_extensive(): for peer_name in reactor.peers(): if limit and limit not in reactor.neighbor_name(peer_name): continue - for line in Neighbor.extensive(reactor.neighbor_cli_data(peer_name)).split('\n'): + for line in NeighborTemplate.extensive(reactor.neighbor_cli_data(peer_name)).split('\n'): reactor.processes.write(service, line) yield True - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) def callback_summary(): - reactor.processes.write(service, Neighbor.summary_header) + reactor.processes.write(service, NeighborTemplate.summary_header) for peer_name in reactor.peers(): if limit and limit != reactor.neighbor_ip(peer_name): continue - for line in Neighbor.summary(reactor.neighbor_cli_data(peer_name)).split('\n'): + for line in NeighborTemplate.summary(reactor.neighbor_cli_data(peer_name)).split('\n'): reactor.processes.write(service, line) yield True - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) if use_json: - reactor.asynchronous.schedule(service, command, callback_json()) + reactor.asynchronous.schedule(service, line, callback_json()) return True if summary: - reactor.asynchronous.schedule(service, command, callback_summary()) + reactor.asynchronous.schedule(service, line, callback_summary()) return True if extensive: - reactor.asynchronous.schedule(service, command, callback_extensive()) + reactor.asynchronous.schedule(service, line, callback_extensive()) return True if configuration: - reactor.asynchronous.schedule(service, command, callback_configuration()) + reactor.asynchronous.schedule(service, line, callback_configuration()) return True reactor.processes.write(service, 'please specify summary, extensive or configuration') reactor.processes.write(service, 'you can filter by peer ip address adding it after the word neighbor') - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) diff --git a/src/exabgp/reactor/api/command/reactor.py b/src/exabgp/reactor/api/command/reactor.py index c1611bd73..8ad7f14a1 100644 --- a/src/exabgp/reactor/api/command/reactor.py +++ b/src/exabgp/reactor/api/command/reactor.py @@ -17,10 +17,11 @@ def register_reactor(): pass -@Command.register('text', 'help', False) -def manual(self, reactor, service, _): +@Command.register('help', False) +def manual(self, reactor, service, line, use_json): lines = [] - for command in sorted(self.callback['text']): + encoding = 'json' if use_json else 'text' + for command in sorted(self.callback[encoding]): if self.callback['options'][command]: extended = '%s [ %s ]' % (command, ' | '.join(self.callback['options'][command])) else: @@ -41,55 +42,55 @@ def manual(self, reactor, service, _): for line in sorted(lines): reactor.processes.write(service, line, True) reactor.processes.write(service, '', True) - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) return True -@Command.register('text', 'shutdown', False) -def shutdown(self, reactor, service, _): +@Command.register('shutdown', False) +def shutdown(self, reactor, service, line, use_json): reactor.signal.received = reactor.signal.SHUTDOWN reactor.processes.write(service, 'shutdown in progress') - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) return True -@Command.register('text', 'reload', False) -def reload(self, reactor, service, _): +@Command.register('reload', False) +def reload(self, reactor, service, line, use_json): reactor.signal.received = reactor.signal.RELOAD reactor.processes.write(service, 'reload in progress') - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) return True -@Command.register('text', 'restart', False) -def restart(self, reactor, service, _): +@Command.register('restart', False) +def restart(self, reactor, service, line, use_json): reactor.signal.received = reactor.signal.RESTART reactor.processes.write(service, 'restart in progress') - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) return True -@Command.register('text', 'version', False) -def version(self, reactor, service, _): +@Command.register('version', False) +def version(self, reactor, service, line, use_json): reactor.processes.write(service, 'exabgp %s' % _version) - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) return True -@Command.register('text', '#', False) -def comment(self, reactor, service, line): +@Command.register('#', False) +def comment(self, reactor, service, line, use_json): log.debug(line.lstrip().lstrip('#').strip(), 'process') - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) return True -@Command.register('text', 'reset', False) -def reset(self, reactor, service, line): +@Command.register('reset', False) +def reset(self, reactor, service, line, use_json): reactor.asynchronous.clear(service) -@Command.register('text', 'crash') -def crash(self, reactor, service, line): +@Command.register('crash') +def crash(self, reactor, service, line, use_json): def callback(): raise ValueError('crash test of the API') yield None diff --git a/src/exabgp/reactor/api/command/rib.py b/src/exabgp/reactor/api/command/rib.py index 8306a6b3a..de6c61523 100644 --- a/src/exabgp/reactor/api/command/rib.py +++ b/src/exabgp/reactor/api/command/rib.py @@ -7,9 +7,12 @@ License: 3-clause BSD. (See the COPYRIGHT file) """ +import json + from exabgp.reactor.api.command.command import Command from exabgp.reactor.api.command.limit import match_neighbors from exabgp.reactor.api.command.limit import extract_neighbors +from exabgp.reactor.api.command.neighbor import NeighborTemplate from exabgp.bgp.message.update.nlri.nlri import NLRI from exabgp.bgp.message.update.nlri.inet import INET @@ -24,7 +27,43 @@ def register_rib(): pass -def _show_adjrib_callback(reactor, service, last, route_type, advertised, rib_name, extensive): +def _show_adjrib_callback(reactor, service, last, route_type, advertised, rib_name, extensive, use_json): + def to_text(key, changes): + for change in changes: + if not isinstance(change.nlri, route_type): + # log something about this drop? + continue + + msg = '%s %s %s' % ( + reactor.neighbor_name(key) if extensive else reactor.neighbor_ip(key), + '%s %s' % change.nlri.family().afi_safi(), + change.extensive() if extensive else str(change.nlri), + ) + reactor.processes.write(service, msg) + + def to_json(key, changes): + jason = {} + neighbor = reactor.neighbor(key) + neighbor_ip = reactor.neighbor_ip(key) + routes = jason.setdefault(neighbor_ip, {'routes': []})['routes'] + + if extensive: + jason[neighbor_ip].update(NeighborTemplate.to_json(neighbor)) + + for change in changes: + if not isinstance(change.nlri, route_type): + # log something about this drop? + continue + + routes.append({ + "prefix": str(change.nlri.cidr.prefix()), + "family": str(change.nlri.family()).strip("()").replace(",", "") + }) + + for line in json.dumps(jason).split('\n'): + reactor.processes.write(service, line) + + def callback(): lines_per_yield = getenv().api.chunk if last in ('routes', 'extensive', 'static', 'flow', 'l2vpn'): @@ -35,57 +74,25 @@ def callback(): routes = reactor.neighor_rib(key, rib_name, advertised) while routes: changes, routes = routes[:lines_per_yield], routes[lines_per_yield:] - for change in changes: - if isinstance(change.nlri, route_type): - if extensive: - reactor.processes.write( - service, - '%s %s %s' - % ( - reactor.neighbor_name(key), - '%s %s' % change.nlri.family().afi_safi(), - change.extensive(), - ), - ) - else: - reactor.processes.write( - service, - 'neighbor %s %s %s' - % ( - reactor.neighbor_ip(key), - '%s %s' % change.nlri.family().afi_safi(), - str(change.nlri), - ), - ) + if use_json: + to_json(key, changes) + else: + to_text(key, changes) yield True - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) return callback -@Command.register( - 'text', - 'show adj-rib out', - False, - [ - 'extensive', - ], -) -@Command.register( - 'text', - 'show adj-rib in', - False, - [ - 'extensive', - ], -) -def show_adj_rib(self, reactor, service, line): +@Command.register('show adj-rib out', False, ['extensive',], True) +@Command.register('show adj-rib in', False, ['extensive',], True) +def show_adj_rib(self, reactor, service, line, use_json): words = line.split() extensive = line.endswith(' extensive') try: rib = words[2] if not rib in ('in', 'out'): - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False except IndexError: if words[1] == 'adj-rib-in': @@ -93,11 +100,11 @@ def show_adj_rib(self, reactor, service, line): elif words[1] == 'adj-rib-out': rib = 'out' else: - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False if rib not in ('in', 'out'): - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False klass = NLRI @@ -109,17 +116,22 @@ def show_adj_rib(self, reactor, service, line): elif 'l2vpn' in words: klass = (VPLS, EVPN) + use_json = False + if 'json' in words: + words.remove('json') + use_json = True + for remove in ('show', 'adj-rib', 'adj-rib-in', 'adj-rib-out', 'in', 'out', 'extensive'): if remove in words: words.remove(remove) last = '' if not words else words[0] - callback = _show_adjrib_callback(reactor, service, last, klass, False, rib, extensive) + callback = _show_adjrib_callback(reactor, service, last, klass, False, rib, extensive, use_json) reactor.asynchronous.schedule(service, line, callback()) return True -@Command.register('text', 'flush adj-rib out') -def flush_adj_rib_out(self, reactor, service, line): +@Command.register('flush adj-rib out') +def flush_adj_rib_out(self, reactor, service, line, use_json): def callback(self, peers): self.log_message( "flushing adjb-rib out for %s" % ', '.join(peers if peers else []) if peers is not None else 'all peers' @@ -128,29 +140,29 @@ def callback(self, peers): reactor.neighbor_rib_resend(peer_name) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) try: descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.established_peers(), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command, 'warning') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False reactor.asynchronous.schedule(service, command, callback(self, peers)) return True except ValueError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False except IndexError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False -@Command.register('text', 'clear adj-rib') -def clear_adj_rib(self, reactor, service, line): +@Command.register('clear adj-rib') +def clear_adj_rib(self, reactor, service, line, use_json): def callback(self, peers, direction): self.log_message( "clearing adjb-rib-%s for %s" @@ -163,24 +175,24 @@ def callback(self, peers, direction): reactor.neighbor_rib_in_clear(peer_name) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) try: descriptions, command = extract_neighbors(line) peers = match_neighbors(reactor.peers(), descriptions) if not peers: self.log_failure('no neighbor matching the command : %s' % command, 'warning') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False - words = line.split() + words = command.split() direction = 'in' if 'adj-rib-in' in words or 'in' in words else 'out' reactor.asynchronous.schedule(service, command, callback(self, peers, direction)) return True except ValueError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False except IndexError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_text_error(service) return False diff --git a/src/exabgp/reactor/api/command/watchdog.py b/src/exabgp/reactor/api/command/watchdog.py index 859ef2da2..d543393ad 100644 --- a/src/exabgp/reactor/api/command/watchdog.py +++ b/src/exabgp/reactor/api/command/watchdog.py @@ -14,8 +14,8 @@ def register_watchdog(): pass -@Command.register('text', 'announce watchdog') -def announce_watchdog(self, reactor, service, line): +@Command.register('announce watchdog') +def announce_watchdog(self, reactor, service, line, use_json): def callback(name): # XXX: move into Action for neighbor_name in reactor.configuration.neighbors.keys(): @@ -25,7 +25,7 @@ def callback(name): neighbor.rib.outgoing.announce_watchdog(name) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) try: name = line.split(' ')[2] @@ -35,8 +35,8 @@ def callback(name): return True -@Command.register('text', 'withdraw watchdog') -def withdraw_watchdog(self, reactor, service, line): +@Command.register('withdraw watchdog') +def withdraw_watchdog(self, reactor, service, line, use_json): def callback(name): # XXX: move into Action for neighbor_name in reactor.configuration.neighbors.keys(): @@ -46,7 +46,7 @@ def callback(name): neighbor.rib.outgoing.withdraw_watchdog(name) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_text_done(service) try: name = line.split(' ')[2] diff --git a/src/exabgp/reactor/api/processes.py b/src/exabgp/reactor/api/processes.py index 26e5feaae..51156f881 100644 --- a/src/exabgp/reactor/api/processes.py +++ b/src/exabgp/reactor/api/processes.py @@ -321,11 +321,17 @@ def _answer(self, service, string, force=False): log.debug('responding to %s : %s' % (service, string.replace('\n', '\\n')), 'process') self.write(service, string) - def answer_done(self, service): - self._answer(service, Answer.done) + def answer_text_done(self, service): + self._answer(service, Answer.text_done) - def answer_error(self, service): - self._answer(service, Answer.error) + def answer_json_done(self, service): + self._answer(service, Answer.json_done) + + def answer_text_error(self, service): + self._answer(service, Answer.text_error) + + def answer_json_error(self, service): + self._answer(service, Answer.json_error) def _notify(self, neighbor, event): for process in neighbor.api[event]: diff --git a/src/exabgp/reactor/api/response/answer.py b/src/exabgp/reactor/api/response/answer.py index 852d815d1..db6209d72 100644 --- a/src/exabgp/reactor/api/response/answer.py +++ b/src/exabgp/reactor/api/response/answer.py @@ -1,6 +1,11 @@ class Answer: - error = 'error' - done = 'done' - shutdown = 'shutdown' + text_error = 'error' + json_error = '{ "answer": "error", "message": "this command does not support json output" }' + text_done = 'done' + json_done = '{ "answer": "done", "message": "command completed" }' + text_shutdown = 'shutdown' + json_shutdown = '{ "answer": "shutdown", "message": "exbgp exited" }' - buffer_size = max(len(error), len(done), len(shutdown)) + text_buffer_size = max(len(text_error), len(text_done), len(text_shutdown)) + json_buffer_size = max(len(json_error), len(json_done), len(json_shutdown)) + buffer_size = max(text_buffer_size, json_buffer_size) diff --git a/src/exabgp/reactor/loop.py b/src/exabgp/reactor/loop.py index af83b5491..289e067e2 100644 --- a/src/exabgp/reactor/loop.py +++ b/src/exabgp/reactor/loop.py @@ -418,7 +418,7 @@ def run(self): # read at least on message per process if there is some and parse it for service, command in self.processes.received(): - self.api.text(self, service, command) + self.api.process(self, service, command) sleep = 0 self.asynchronous.run()