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..28bcb523b 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.bgp.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..812b2c454 100644 --- a/src/exabgp/bgp/neighbor.py +++ b/src/exabgp/bgp/neighbor.py @@ -7,12 +7,16 @@ License: 3-clause BSD. (See the COPYRIGHT file) """ +import json + from copy import deepcopy from collections import deque from collections import Counter +from datetime import timedelta + from exabgp.protocol.family import AFI from exabgp.util.dns import host, domain @@ -290,24 +294,104 @@ def __eq__(self, other): def __ne__(self, other): return not self.__eq__(other) - def string(self, with_changes=True): + def ip_self(self, afi): + if afi == self['local-address'].afi: + return self['local-address'] + + # attempting to not barf for next-hop self when the peer is IPv6 + if afi == AFI.ipv4: + return self['router-id'] + + raise TypeError( + 'use of "next-hop self": the route (%s) does not have the same family as the BGP tcp session (%s)' + % (afi, self['local-address'].afi) + ) + + def remove_self(self, changes): + change = deepcopy(changes) + if not change.nlri.nexthop.SELF: + return change + neighbor_self = self.ip_self(change.nlri.afi) + change.nlri.nexthop = neighbor_self + if Attribute.CODE.NEXT_HOP in change.attributes: + change.attributes[Attribute.CODE.NEXT_HOP] = NextHop(str(neighbor_self), neighbor_self.pack()) + return change + + def __str__(self): + return NeighborTemplate.configuration(self, False) + + +def _en(value): + if value is None: + return 'n/a' + return 'enabled' if value else 'disabled' + + +def _pr(value): + if value is None: + return 'n/a' + return '%s' % value + + +def _addpath(send, receive): + if send and receive: + return "send/receive" + if send: + return "send" + if receive: + return "receive" + return "disabled" + + +class NeighborTemplate(object): + extensive_kv = ' %-20s %15s %15s %15s' + extensive_template = """\ +Neighbor %(peer-address)s + + Session Local +%(local-address)s +%(state)s +%(duration)s + + Setup Local Remote +%(as)s +%(id)s +%(hold)s + + Capability Local Remote +%(capabilities)s + + Families Local Remote Add-Path +%(families)s + + Message Statistic Sent Received +%(messages)s +""".replace( + '\t', ' ' + ) + + 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 self.rib.outgoing.queued_changes(): + for change in neighbor.rib.outgoing.queued_changes(): changes += '\n\t\t%s' % change.extensive() changes += '\n}' families = '' - for afi, safi in self.families(): + for afi, safi in neighbor.families(): families += '\n\t\t%s %s;' % (afi.name(), safi.name()) nexthops = '' - for afi, safi, nexthop in self.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 self.addpaths(): + for afi, safi in neighbor.addpaths(): addpaths += '\n\t\t%s %s;' % (afi.name(), safi.name()) codes = Message.CODE @@ -345,7 +429,7 @@ def string(self, with_changes=True): apis = '' - for process in self.api.get('processes', []): + for process in neighbor.api.get('processes', []): _global = [] _receive = [] _send = [] @@ -355,7 +439,7 @@ def string(self, with_changes=True): [ '\t\t%s;\n' % name, ] - if process in self.api[api] + if process in neighbor.api[api] else [] ) @@ -364,7 +448,7 @@ def string(self, with_changes=True): [ '\t\t\t%s;\n' % name, ] - if process in self.api[api] + if process in neighbor.api[api] else [] ) @@ -373,7 +457,7 @@ def string(self, with_changes=True): [ '\t\t\t%s;\n' % name, ] - if process in self.api[api] + if process in neighbor.api[api] else [] ) @@ -418,42 +502,42 @@ def string(self, with_changes=True): '%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 '', + 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 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'), + % ('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' - % (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'), + % (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[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'), + % (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, @@ -466,28 +550,108 @@ def string(self, with_changes=True): # '\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'] - # attempting to not barf for next-hop self when the peer is IPv6 - if afi == AFI.ipv4: - return self['router-id'] + @classmethod + def as_dict(cls, answer): + up = answer['duration'] + + formated = { + 'state': 'up' if up else 'down', + 'duration': answer['duration'] if up else answer['down'], + 'fsm': answer['state'], + 'local': { + 'capabilities': {}, + 'families': {}, + 'add-path': {}, + }, + 'peer': { + 'capabilities': {}, + 'families': {}, + 'add-path': {}, + }, + 'messages': {'sent': {}, 'received': {}}, + 'capabilities': [], + 'families': [], + 'add-path': {}, + } - raise TypeError( - 'use of "next-hop self": the route (%s) does not have the same family as the BGP tcp session (%s)' - % (afi, self['local-address'].afi) - ) + for (a, s), (l, p, aps, apr) in answer['families'].items(): + k = '%s %s' % (a, s) + formated['local']['families'][k] = l + formated['peer']['families'][k] = p + formated['local']['add-path'][k] = aps + formated['peer']['add-path'][k] = apr + if l and p: + formated['families'].append(k) + formated['add-path'][k] = _addpath(aps, apr) + + for k, (l, p) in answer['capabilities'].items(): + formated['local']['capabilities'][k] = l + formated['peer']['capabilities'][k] = p + if l and p: + formated['capabilities'].append(k) + + for k, (s, r) in answer['messages'].items(): + formated['messages']['sent'][k] = s + formated['messages']['received'][k] = r + + formated['local']['address'] = answer['local-address'] + formated['local']['as'] = answer['local-as'] + formated['local']['id'] = answer['local-id'] + formated['local']['hold'] = answer['local-hold'] + + formated['peer']['address'] = answer['peer-address'] + formated['peer']['as'] = answer['peer-as'] + formated['peer']['id'] = answer['peer-id'] + formated['peer']['hold'] = answer['peer-hold'] + + return formated + + @classmethod + def formated_dict(cls, answer): + if answer['duration']: + duration = cls.extensive_kv % ('up for', timedelta(seconds=answer['duration']), '', '') + else: + duration = cls.extensive_kv % ('down for', timedelta(seconds=answer['down']), '', '') + + formated = { + 'peer-address': answer['peer-address'], + 'local-address': cls.extensive_kv % ('local', answer['local-address'], '', ''), + 'state': cls.extensive_kv % ('state', answer['state'], '', ''), + 'duration': duration, + 'as': cls.extensive_kv % ('AS', answer['local-as'], _pr(answer['peer-as']), ''), + 'id': cls.extensive_kv % ('ID', answer['local-id'], _pr(answer['peer-id']), ''), + 'hold': cls.extensive_kv % ('hold-time', answer['local-hold'], _pr(answer['peer-hold']), ''), + 'capabilities': '\n'.join( + cls.extensive_kv % ('%s:' % k, _en(l), _en(p), '') for k, (l, p) in answer['capabilities'].items() + ), + 'families': '\n'.join( + cls.extensive_kv % ('%s %s:' % (a, s), _en(l), _en(r), _addpath(aps, apr)) + for (a, s), (l, r, apr, aps) in answer['families'].items() + ), + 'messages': '\n'.join( + cls.extensive_kv % ('%s:' % k, str(s), str(r), '') for k, (s, r) in answer['messages'].items() + ), + } - def remove_self(self, changes): - change = deepcopy(changes) - if not change.nlri.nexthop.SELF: - return change - neighbor_self = self.ip_self(change.nlri.afi) - change.nlri.nexthop = neighbor_self - if Attribute.CODE.NEXT_HOP in change.attributes: - change.attributes[Attribute.CODE.NEXT_HOP] = NextHop(str(neighbor_self), neighbor_self.pack()) - return change + 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) + + @classmethod + def summary(cls, answer): + return cls.summary_template % ( + answer['peer-address'], + _pr(answer['peer-as']), + timedelta(seconds=answer['duration']) if answer['duration'] else 'down', + answer['state'].lower(), + answer['messages']['update'][0], + answer['messages']['update'][1], + ) - def __str__(self): - return self.string(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..56580d78e 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_error(service, use_json) 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_error(service, use_json) yield True return @@ -56,36 +56,36 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) except ValueError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) yield True except IndexError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_error(service, use_json) 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_error(service, use_json) yield True return @@ -112,36 +112,36 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) except ValueError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) yield True except IndexError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_error(service, use_json) 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_error(service, use_json) yield True return @@ -153,29 +153,29 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) except ValueError: self.log_failure('issue parsing the vpls') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) yield True except IndexError: self.log_failure('issue parsing the vpls') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_error(service, use_json) 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_error(service, use_json) yield True return @@ -200,37 +200,37 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) except ValueError: self.log_failure('issue parsing the vpls') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) yield True except IndexError: self.log_failure('issue parsing the vpls') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_error(service, use_json) 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_error(service, use_json) yield True return @@ -242,37 +242,37 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) except ValueError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) yield True except IndexError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_error(service, use_json) 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_error(service, use_json) yield True return @@ -289,36 +289,36 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) except ValueError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) yield True except IndexError: self.log_failure('issue parsing the route') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_error(service, use_json) 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_error(service, use_json) yield True return @@ -330,29 +330,29 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) except ValueError: self.log_failure('issue parsing the flow') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) yield True except IndexError: self.log_failure('issue parsing the flow') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_error(service, use_json) 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_error(service, use_json) yield True return @@ -376,27 +376,27 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) except ValueError: self.log_failure('issue parsing the flow') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) yield True except IndexError: self.log_failure('issue parsing the flow') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_error(service, use_json) yield True return @@ -407,34 +407,34 @@ def callback(self, command, peers): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) 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_error(service, use_json) 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_error(service, use_json) return False except IndexError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_error(service, use_json) yield True return @@ -446,34 +446,34 @@ def callback(self, command, peers): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) 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_error(service, use_json) 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_error(service, use_json) return False except IndexError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_error(service, use_json) yield True return @@ -483,9 +483,9 @@ 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_done(service, use_json) - if (command.split() + ['be', 'safe'])[2].lower() not in ( + if (line.split() + ['be', 'safe'])[2].lower() not in ( 'asm', 'adm', 'rpcq', @@ -495,44 +495,44 @@ def callback(self, command, peers): 'lpcq', 'lpcp', ): - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) 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_error(service, use_json) 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_error(service, use_json) return False except IndexError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_error(service, use_json) 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_error(service, use_json) yield True return @@ -544,29 +544,29 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) except ValueError: self.log_failure('issue parsing the ipv4') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) yield True except IndexError: self.log_failure('issue parsing the ipv4') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_error(service, use_json) 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_error(service, use_json) yield True return @@ -590,36 +590,36 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) except ValueError: self.log_failure('issue parsing the ipv4') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) yield True except IndexError: self.log_failure('issue parsing the ipv4') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_error(service, use_json) 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_error(service, use_json) yield True return @@ -631,29 +631,29 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) except ValueError: self.log_failure('issue parsing the ipv6') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) yield True except IndexError: self.log_failure('issue parsing the ipv6') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_error(service, use_json) 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_error(service, use_json) yield True return @@ -677,14 +677,14 @@ def callback(): ) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) except ValueError: self.log_failure('issue parsing the ipv6') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) yield True except IndexError: self.log_failure('issue parsing the ipv6') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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 c61b645b0..444a0a46b 100644 --- a/src/exabgp/reactor/api/command/neighbor.py +++ b/src/exabgp/reactor/api/command/neighbor.py @@ -9,204 +9,53 @@ import json -from datetime import timedelta +from exabgp.bgp.neighbor import NeighborTemplate -from exabgp.reactor.api.command.command import Command from exabgp.reactor.api.command.limit import match_neighbor from exabgp.reactor.api.command.limit import extract_neighbors +from exabgp.reactor.api.command.command import Command + def register_neighbor(): pass -def _en(value): - if value is None: - return 'n/a' - return 'enabled' if value else 'disabled' - - -def _pr(value): - if value is None: - return 'n/a' - return '%s' % value - - -def _addpath(send, receive): - if send and receive: - return "send/receive" - if send: - return "send" - if receive: - return "receive" - return "disabled" - - -class Neighbor(object): - extensive_kv = ' %-20s %15s %15s %15s' - extensive_template = """\ -Neighbor %(peer-address)s - - Session Local -%(local-address)s -%(state)s -%(duration)s - - Setup Local Remote -%(as)s -%(id)s -%(hold)s - - Capability Local Remote -%(capabilities)s - - Families Local Remote Add-Path -%(families)s - - Message Statistic Sent Received -%(messages)s -""".replace( - '\t', ' ' - ) - - summary_header = 'Peer AS up/down state | #sent #recvd' - summary_template = '%-15s %-7s %9s %-12s %10d %10d' - - @classmethod - def as_dict(cls, answer): - up = answer['duration'] - - formated = { - 'state': 'up' if up else 'down', - 'duration': answer['duration'] if up else answer['down'], - 'fsm': answer['state'], - 'local': { - 'capabilities': {}, - 'families': {}, - 'add-path': {}, - }, - 'peer': { - 'capabilities': {}, - 'families': {}, - 'add-path': {}, - }, - 'messages': {'sent': {}, 'received': {}}, - 'capabilities': [], - 'families': [], - 'add-path': {}, - } - - for (a, s), (l, p, aps, apr) in answer['families'].items(): - k = '%s %s' % (a, s) - formated['local']['families'][k] = l - formated['peer']['families'][k] = p - formated['local']['add-path'][k] = aps - formated['peer']['add-path'][k] = apr - if l and p: - formated['families'].append(k) - formated['add-path'][k] = _addpath(aps, apr) - - for k, (l, p) in answer['capabilities'].items(): - formated['local']['capabilities'][k] = l - formated['peer']['capabilities'][k] = p - if l and p: - formated['capabilities'].append(k) - - for k, (s, r) in answer['messages'].items(): - formated['messages']['sent'][k] = s - formated['messages']['received'][k] = r - - formated['local']['address'] = answer['local-address'] - formated['local']['as'] = answer['local-as'] - formated['local']['id'] = answer['local-id'] - formated['local']['hold'] = answer['local-hold'] - - formated['peer']['address'] = answer['peer-address'] - formated['peer']['as'] = answer['peer-as'] - formated['peer']['id'] = answer['peer-id'] - formated['peer']['hold'] = answer['peer-hold'] - - return formated - - @classmethod - def formated_dict(cls, answer): - if answer['duration']: - duration = cls.extensive_kv % ('up for', timedelta(seconds=answer['duration']), '', '') - else: - duration = cls.extensive_kv % ('down for', timedelta(seconds=answer['down']), '', '') - - formated = { - 'peer-address': answer['peer-address'], - 'local-address': cls.extensive_kv % ('local', answer['local-address'], '', ''), - 'state': cls.extensive_kv % ('state', answer['state'], '', ''), - 'duration': duration, - 'as': cls.extensive_kv % ('AS', answer['local-as'], _pr(answer['peer-as']), ''), - 'id': cls.extensive_kv % ('ID', answer['local-id'], _pr(answer['peer-id']), ''), - 'hold': cls.extensive_kv % ('hold-time', answer['local-hold'], _pr(answer['peer-hold']), ''), - 'capabilities': '\n'.join( - cls.extensive_kv % ('%s:' % k, _en(l), _en(p), '') for k, (l, p) in answer['capabilities'].items() - ), - 'families': '\n'.join( - cls.extensive_kv % ('%s %s:' % (a, s), _en(l), _en(r), _addpath(aps, apr)) - for (a, s), (l, r, apr, aps) in answer['families'].items() - ), - 'messages': '\n'.join( - cls.extensive_kv % ('%s:' % k, str(s), str(r), '') for k, (s, r) in answer['messages'].items() - ), - } - - return formated - - @classmethod - def extensive(cls, answer): - return cls.extensive_template % cls.formated_dict(answer) - - @classmethod - def summary(cls, answer): - return cls.summary_template % ( - answer['peer-address'], - _pr(answer['peer-as']), - timedelta(seconds=answer['duration']) if answer['duration'] else 'down', - answer['state'].lower(), - answer['messages']['update'][0], - answer['messages']['update'][1], - ) - - -@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_error(service, use_json) return False _, code = line.split(' ', 1) if not code.isdigit(): - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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_done(service, use_json) return True except ValueError: - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) return False except IndexError: - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) return False -@Command.register('text', 'show neighbor', False, ['summary', 'extensive', 'configuration', 'json']) -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 summary = 'summary' in words jason = 'json' in words + text = 'text' in words if summary: words.remove('summary') @@ -216,6 +65,8 @@ def show_neighbor(self, reactor, service, command): words.remove('configuration') if jason: words.remove('json') + if text: + words.remove('text') limit = words[-1] if words[-1] != 'neighbor' else '' @@ -229,52 +80,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_done(service, use_json) 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_done(service, use_json) 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_done(service, use_json) 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_done(service, use_json) - if jason: - reactor.asynchronous.schedule(service, command, callback_json()) + if use_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_done(service, use_json) 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..d9466c291 100644 --- a/src/exabgp/reactor/api/command/rib.py +++ b/src/exabgp/reactor/api/command/rib.py @@ -7,6 +7,10 @@ License: 3-clause BSD. (See the COPYRIGHT file) """ +import json + +from exabgp.bgp.neighbor import NeighborTemplate + 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 @@ -24,7 +28,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 +75,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_done(service, use_json) 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_error(service, use_json) return False except IndexError: if words[1] == 'adj-rib-in': @@ -93,11 +101,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_error(service, use_json) return False if rib not in ('in', 'out'): - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) return False klass = NLRI @@ -109,17 +117,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 +141,29 @@ def callback(self, peers): reactor.neighbor_rib_resend(peer_name) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) 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_error(service, use_json) 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_error(service, use_json) return False except IndexError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) 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 +176,24 @@ def callback(self, peers, direction): reactor.neighbor_rib_in_clear(peer_name) yield False - reactor.processes.answer_done(service) + reactor.processes.answer_done(service, use_json) 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_error(service, use_json) 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_error(service, use_json) return False except IndexError: self.log_failure('issue parsing the command') - reactor.processes.answer_error(service) + reactor.processes.answer_error(service, use_json) return False diff --git a/src/exabgp/reactor/api/command/watchdog.py b/src/exabgp/reactor/api/command/watchdog.py index 859ef2da2..1810353f8 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_done(service, use_json) 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_done(service, use_json) try: name = line.split(' ')[2] diff --git a/src/exabgp/reactor/api/processes.py b/src/exabgp/reactor/api/processes.py index 26e5feaae..e1dc93227 100644 --- a/src/exabgp/reactor/api/processes.py +++ b/src/exabgp/reactor/api/processes.py @@ -321,11 +321,29 @@ 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_done(self, service, use_json): + if use_json: + self.answer_json_done(service) + else: + self.answer_text_done(service) + + def answer_text_done(self, service): + self._answer(service, Answer.text_done) + + def answer_json_done(self, service): + self._answer(service, Answer.json_done) + + def answer_error(self, service, use_json): + if use_json: + self.answer_json_error(service) + else: + self.answer_text_error(service) + + def answer_text_error(self, service): + self._answer(service, Answer.text_error) - def answer_error(self, service): - self._answer(service, Answer.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()