ulgbird.py 17.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
#!/usr/bin/env python
#
# ULG - Universal Looking Glass
# by Tomas Hlavacek (tomas.hlavacek@nic.cz)
# last udate: June 21 2012
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


# Imports
import os
import socket
import re

import defaults

import ulgmodel
Tomas Hlavacek's avatar
Tomas Hlavacek committed
29
import ulggraph
30

31
IPV46_SUBNET_REGEXP = '^[0-9a-fA-F:\.]+(/[0-9]{1,2}){0,1}$'
32 33
RTNAME_REGEXP = '^[a-zA-Z0-9]+$'
STRING_SYMBOL_ROUTING_TABLE = 'routing table'
34

35
BIRD_SOCK_HEADER_REGEXP='^([0-9]+)[-\s](.+)$'
36
BIRD_SOCK_REPLY_END_REGEXP='^([0-9]+)\s*(\s.*)?$'
37

38 39
BIRD_SHOW_PROTO_LINE_REGEXP='^\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)(\s+([^\s].+)){0,1}\s*$'
BIRD_SHOW_PROTO_HEADER_REGEXP='^\s*(name)\s+(proto)\s+(table)\s+(state)\s+(since)\s+(info)\s*$'
40

41
BIRD_RT_LINE_REGEXP = '^([^\s]+)\s+via\s+([^\s]+)\s+on\s+([^\s]+)\s+(\[[^\]]+\])\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)'
42
BIRD_ASFIELD_REGEXP = '^\s*\[AS([0-9]+)(i|\?)\]\s*$'
43

Tomas Hlavacek's avatar
Tomas Hlavacek committed
44 45 46
BIRD_SHOW_SYMBOLS_LINE_REGEXP = '^([^\s]+)\s+(.+)\s*'

BIRD_GRAPH_SH_ROUTE_ALL = "Graph show route table <RT> for <IP subnet>"
47

48 49
bird_sock_header_regexp = re.compile(BIRD_SOCK_HEADER_REGEXP)
bird_sock_reply_end_regexp = re.compile(BIRD_SOCK_REPLY_END_REGEXP)
50
bird_rt_line_regexp = re.compile(BIRD_RT_LINE_REGEXP)
51
bird_asfield_regexp = re.compile(BIRD_ASFIELD_REGEXP)
52
bird_show_symbols_line_regexp = re.compile(BIRD_SHOW_SYMBOLS_LINE_REGEXP)
53

Tomas Hlavacek's avatar
Tomas Hlavacek committed
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
BIRD_SH_ROUTE_ALL_ASES_REGEXP = "^\s*BGP\.as_path:\s+([0-9\s]+)\s*$"
bird_sh_route_all_ases_regexp = re.compile(BIRD_SH_ROUTE_ALL_ASES_REGEXP)

BIRD_SH_ROUTE_ALL_NEXTHOP_REGEXP = ".*\s+via\s+([0-9a-fA-F:\.]+)\s+on\s+[^\s]+\s+\[([^\s]+)\s+.*"
bird_sh_route_all_nexthop_regexp = re.compile(BIRD_SH_ROUTE_ALL_NEXTHOP_REGEXP)

BIRD_SH_ROUTE_ALL_USED_REGEXP = ".*\]\s+\*\s+\(.*"
bird_sh_route_all_used_regexp = re.compile(BIRD_SH_ROUTE_ALL_USED_REGEXP)

def bird_parse_sh_route_all(text,prependas):
    def split_ases(ases):
		return str.split(ases)

    DEFAULT_PARAMS = {'recuse':False, 'reconly':False, 'aggr':None}

    res = []
    params = DEFAULT_PARAMS
    for l in str.splitlines(text):
        m = bird_sh_route_all_nexthop_regexp.match(l)
        if(m):
            params['peer'] = m.group(2)
            if(bird_sh_route_all_used_regexp.match(l)):
                params['recuse'] = True

        m = bird_sh_route_all_ases_regexp.match(l)
        if(m):
            ases = ["AS"+str(asn) for asn in [prependas] + split_ases(m.group(1))]
            res.append((ases,params))
            params = DEFAULT_PARAMS
            continue

    return res

def bird_reduce_paths(paths):
    def assign_value(path):
        if(path[1]['recuse']):
            return 1
        elif(path[1]['reconly']):
            return 100
        else:
            return 10

    return sorted(paths,key=assign_value)

98
def parseBirdShowProtocols(text,resrange=None):
99 100 101 102 103 104 105
    def parseShowProtocolsLine(line):
        sh_proto_line_regexp = re.compile(BIRD_SHOW_PROTO_LINE_REGEXP)
        m = sh_proto_line_regexp.match(line)
        if(m):
            res = list(m.groups()[0:5])
            if(m.group(6)):
                res.append(m.group(6))
106

107 108
            return res
        else:
109
            ulgmodel.log("WARN: bird.parseShowProtocolsLine failed to match line: "+line)
110
            return None
111

112 113 114

    header = []
    table = []
115

116 117 118 119 120 121 122 123 124 125 126 127
    for l in str.splitlines(text):
        if(re.match('^\s*$',l)):
            continue
        
        hm = re.match(BIRD_SHOW_PROTO_HEADER_REGEXP,l)
        if(hm):
            header = hm.groups()
        else:
            pl = parseShowProtocolsLine(l)
            if(pl):
                table.append(pl)
            else:
128
                ulgmodel.log("ulgbird.parseBirdShowProtocols skipping unparsable line"+l)
129

130 131 132 133
    if(resrange):
        return (header,table[resrange:resrange+defaults.range_step],len(table))
    else:
        return (header,table,len(table))
134 135 136 137 138 139

# classes

class BirdShowProtocolsCommand(ulgmodel.TextCommand):
    COMMAND_TEXT = 'show protocols'

140
    def __init__(self,name=None,show_proto_all_command=None,proto_filter=None):
141
        ulgmodel.TextCommand.__init__(self,self.COMMAND_TEXT,param_specs=[],name=name)
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
        self.show_proto_all_command = show_proto_all_command
        self.fltr = proto_filter

    def _getPeerURL(self,decorator_helper,router,peer_id):
        if decorator_helper and self.show_proto_all_command:
            return decorator_helper.getRuncommandURL({'routerid':str(decorator_helper.getRouterID(router)),
                                                      'commandid':str(decorator_helper.getCommandID(router,self.show_proto_all_command)),
                                                      'param0':peer_id})
        else:
            return None

    def _getPeerTableCell(self,decorator_helper,router,peer_id):
        url = self._getPeerURL(decorator_helper,router,peer_id)
        if(url):
            return decorator_helper.ahref(url,peer_id)
        else:
            return peer_id
159

160
    def _decorateTableLine(self,table_line,router,decorator_helper):
161 162 163 164 165 166 167 168 169
        def _getTableLineColor(state):
            if(state == 'up'):
                return ulgmodel.TableDecorator.GREEN
            elif(state == 'start'):
                return ulgmodel.TableDecorator.YELLOW
            else:
                return ulgmodel.TableDecorator.RED

        color = _getTableLineColor(table_line[3])
170 171 172 173 174 175 176 177 178 179
        tl = [(self._getPeerTableCell(decorator_helper,router,table_line[0]),color),
              (table_line[1],color),
              (table_line[2],color),
              (table_line[3],color),
              (table_line[4],color),
              ]
        if(len(table_line)>5):
            tl.append((table_line[5],color))

        return tl
180

181

182 183 184 185 186 187
    def decorateResult(self,session,decorator_helper=None):
        if(not session):
            raise Exception("Can not decorate result without valid session.")

        if((not session.getRouter()) or (not decorator_helper)):
            return "<pre>\n%s\n</pre>" % session.getResult()
188
        else:
189
            pr = parseBirdShowProtocols(session.getResult(),session.getRange())
190
            table_header = pr[0]
191 192
            table = []

193
            for tl in pr[1][session.getRange():session.getRange()+defaults.range_step]:
194 195 196
                # skip when there is a filter and it does not match the protocol type
                if(self.fltr) and (not re.match(self.fltr,tl[1])):
                    continue
197
                table.append(self._decorateTableLine(tl,session.getRouter(),decorator_helper))
198

199
            return (ulgmodel.TableDecorator(table,table_header).decorate(),pr[2])
200 201


202 203
class BirdBGPPeerSelectCommand(ulgmodel.TextCommand):
    """ Abstract class for all BIRD BGP peer-specific commands """
204 205 206 207 208 209

    def __init__(self,peers,name=None):
        peer_param = ulgmodel.SelectionParameter([tuple((p,p,)) for p in peers],
                                                 name=defaults.STRING_PEERID)
        ulgmodel.TextCommand.__init__(self,self.COMMAND_TEXT,param_specs=[peer_param],name=name)

210 211 212 213 214 215
class BirdShowProtocolsAllCommand(BirdBGPPeerSelectCommand):
    COMMAND_TEXT = 'show protocols all %s'

class BirdShowRouteExportCommand(BirdBGPPeerSelectCommand):
    COMMAND_TEXT = 'show route export %s'

216
class BirdShowRouteCommand(ulgmodel.TextCommand):
217 218 219 220 221
    COMMAND_TEXT = 'show route table %s for %s'

    def __init__(self,tables,name=None):
        table_param = ulgmodel.SelectionParameter([tuple((t,t,)) for t in tables],
                                                  name=defaults.STRING_RTABLE)
222 223

        ulgmodel.TextCommand.__init__(self,self.COMMAND_TEXT,param_specs=[
224 225
                table_param,
                ulgmodel.TextParameter(pattern=IPV46_SUBNET_REGEXP,name=defaults.STRING_IPSUBNET),
226 227
                ],name=name)

228

229 230 231 232
    def _decorateOriginAS(self,asfield,decorator_helper):
        # expected input is "[AS28171i]"
        m = bird_asfield_regexp.match(asfield)
        if(m):
233
            return '['+decorator_helper.mwin(defaults.getASNURL(m.group(1)),'AS'+m.group(1))+m.group(2)+']'
234 235 236
        else:
            return asfield

237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
    def _genTable(self,table_lines,decorator_helper,router):
        def matchBIRDBGPRTLine(line):
            m = bird_rt_line_regexp.match(line)
            if(m):
                return m.groups()
            else:
                ulgmodel.debug("BirdShowRouteProtocolCommand: Can not parse line: "+line)
                return None

        result = []
        for tl in table_lines:
            ml=matchBIRDBGPRTLine(tl)
            if(ml):
                # generate table content
                result.append([
252
                        (decorator_helper.mwin(defaults.getIPPrefixURL(ml[0]),ml[0]),),
253 254 255 256 257
                        (ml[1],),
                        (ml[2],),
                        (ml[3],),
                        (ml[4],),
                        (ml[5],),
258
                        (self._decorateOriginAS(ml[6],decorator_helper),),
259 260 261 262
                        ])
        return result


263 264 265 266 267 268
    def decorateResult(self,session,decorator_helper=None):
        if(not session):
            raise Exception("Can not decorate result without valid session.")

        if((not session.getRouter()) or (not decorator_helper)):
            return "<pre>\n%s\n</pre>" % session.getResult()
269 270 271 272 273 274 275 276 277 278

        table=[]
        table_header=['Prefix',
                      'Next-hop',
                      'Interface',
                      'Since',
                      'Status',
                      'Metric',
                      'Info',]

279
        lines = str.splitlines(session.getResult())
280
        result_len = len(lines)
281 282
        lines = lines[session.getRange():session.getRange()+defaults.range_step]
        table = self._genTable(lines,decorator_helper,session.getRouter())
283

284
        return (ulgmodel.TableDecorator(table,table_header).decorate(),result_len)
285

286 287 288 289 290 291 292 293 294 295
class BirdShowRouteProtocolCommand(BirdShowRouteCommand):
    COMMAND_TEXT = 'show route table %s protocol %s'

    def __init__(self,peers,tables,name=None):
        peer_param = ulgmodel.SelectionParameter([tuple((p,p,)) for p in peers],
                                                 name=defaults.STRING_PEERID)
        table_param = ulgmodel.SelectionParameter([tuple((t,t,)) for t in tables],
                                                  name=defaults.STRING_RTABLE)

        ulgmodel.TextCommand.__init__(self,self.COMMAND_TEXT,param_specs=[table_param, peer_param],name=name)
296

297

298
class BirdShowRouteAllCommand(ulgmodel.TextCommand):
299 300 301 302 303
    COMMAND_TEXT = 'show route table %s all for %s'

    def __init__(self,tables,name=None):
        table_param = ulgmodel.SelectionParameter([tuple((t,t,)) for t in tables],
                                                  name=defaults.STRING_RTABLE)
304

305
        ulgmodel.TextCommand.__init__(self,self.COMMAND_TEXT,param_specs=[
306 307
                table_param,
                ulgmodel.TextParameter(pattern=IPV46_SUBNET_REGEXP,name=defaults.STRING_IPSUBNET),
308 309
                ],
                                      name=name)
310

Tomas Hlavacek's avatar
Tomas Hlavacek committed
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
class BirdGraphShowRouteAll(ulgmodel.TextCommand):
    COMMAND_TEXT = 'show route table %s all for %s'

    def __init__(self,tables,name=BIRD_GRAPH_SH_ROUTE_ALL):
        table_param = ulgmodel.SelectionParameter([tuple((t,t,)) for t in tables],
                                                  name=defaults.STRING_RTABLE)

        ulgmodel.TextCommand.__init__(self,self.COMMAND_TEXT,param_specs=[
                table_param,
                ulgmodel.TextParameter(pattern=IPV46_SUBNET_REGEXP,name=defaults.STRING_IPSUBNET),
                ],
                                      name=name)

    def decorateResult(self,session,decorator_helper=None):
        return (decorator_helper.img(decorator_helper.getSpecialContentURL(session.getSessionId()),"BGP graph"),1)

    def getSpecialContent(self,session,**params):
        pass
        print "Content-type: image/png\n"
	paths = bird_parse_sh_route_all(session.getResult(),str(session.getRouter().getASN()))
        ulggraph.bgp_graph_gen(bird_reduce_paths(paths),start=session.getRouter().getName(),
			       end=session.getParameters()[1])

    def showRange(self):
        return False

337

338
class BirdRouterLocal(ulgmodel.LocalRouter):
339
    RESCAN_PEERS_COMMAND = 'show protocols'
340
    RESCAN_TABLES_COMMAND = 'show symbols'
341
    DEFAULT_PROTOCOL_FLTR = '^(Kernel|Device|Static|BGP).*$'
342

Tomas Hlavacek's avatar
Tomas Hlavacek committed
343
    def __init__(self,sock=defaults.default_bird_sock,commands=None,proto_fltr=None,asn='My ASN'):
344
        super(self.__class__,self).__init__()
345 346
        self.sock = sock
        self.setName('localhost')
Tomas Hlavacek's avatar
Tomas Hlavacek committed
347
        self.setASN(asn)
348 349 350 351
        if(proto_fltr):
            self.proto_fltr = proto_fltr
        else:
            self.proto_fltr = self.DEFAULT_PROTOCOL_FLTR
352

353
        # command autoconfiguration might run only after other parameters are set
354 355 356
        if(commands):
            self.setCommands(commands)
        else:
357 358 359
            self.setCommands(self._getDefaultCommands())

    def _getDefaultCommands(self):
360
        sh_proto_all = BirdShowProtocolsAllCommand(self.getBGPPeers())
361
        sh_proto_route = BirdShowRouteProtocolCommand(self.getBGPPeers(),self.getRoutingTables())
362 363
        sh_proto_export = BirdShowRouteExportCommand(self.getBGPPeers())
        return [BirdShowProtocolsCommand(show_proto_all_command=sh_proto_all, proto_filter = self.proto_fltr),
364
                BirdShowRouteCommand(self.getRoutingTables()),
365 366 367
                sh_proto_all,
                sh_proto_route,
                sh_proto_export,
368
                BirdShowRouteAllCommand(self.getRoutingTables()),
Tomas Hlavacek's avatar
Tomas Hlavacek committed
369
                BirdGraphShowRouteAll(self.getRoutingTables()),
370 371
                ulgmodel.TextCommand('show status'),
                ulgmodel.TextCommand('show memory')
372
                ]
373

374
    def runRawCommand(self,command,outfile):
375 376 377 378 379
        def parseBirdSockLine(line):
            hm = bird_sock_header_regexp.match(line)
            if(hm):
                # first line of the reply
                return (int(hm.group(1)),hm.group(2))
380

381 382 383 384
            em = bird_sock_reply_end_regexp.match(line)
            if(em):
                # most likely the last line of the reply
                return (int(em.group(1)),None)
385

386 387 388
            if(line[0] == '+'):
                # ignore async reply
                return (None,None)
389

390 391 392
            if(line[0] == ' '):
                # return reply line as it is (remove padding)
                return (None,line[1:])
393

394
            raise Exception("Can not parse BIRD output line: "+line)
395

396 397
        def isBirdSockReplyEnd(code):
            if(code==None):
398 399
                return False

400 401 402 403 404
            if(code == 0):
                # end of reply
                return True
            elif(code == 13):
                # show status last line
405
                return True
406 407 408
            elif(code >= 9000):
                # probably error
                return True
409 410 411
            else:
                return False

412 413 414 415 416
#        try:
        # open socket to BIRD
        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        s.settimeout(defaults.default_bird_sock_timeout)
        s.connect(self.sock)
417

418 419
        # cretate FD for the socket
        sf=s.makefile()
420

421 422
        # wait for initial header
        l = sf.readline()
423

424 425 426
        # send the command string
        sf.write(command+"\n")
        sf.flush()
427

428 429 430
        # read and capture lines until the output delimiter string is hit
        while(True):
            l = sf.readline()
431

432
            ulgmodel.debug("Raw line read: " + l)
433

434 435 436 437 438
            # process line according to rules take out from the C code
            lp = parseBirdSockLine(l)
            if(isBirdSockReplyEnd(lp[0])):
                # End of reply (0000 or similar code)
                ulgmodel.debug("End of reply. Code="+str(lp[0]))
439

440 441
                if(lp[1]):
                    ulgmodel.debug("Last read line after normalize: " + lp[1])
442
                    outfile.write(lp[1].rstrip()+"\n")
443 444 445 446
                break
            else:
                if(lp[1]):
                    ulgmodel.debug("Read line after normalize: " + lp[1])
447
                    outfile.write(lp[1].rstrip()+"\n")
448
                else:
449
                    ulgmodel.debug("Read line was empty after normalize.")
450

451 452
        # close the socket and return captured result
        s.close()
453

454 455 456
#        except socket.timeout as e:
#            # catch only timeout exception, while letting other exceptions pass
#            outfile.result(defaults.STRING_SOCKET_TIMEOUT)
457 458 459

    def getForkNeeded(self):
        return False
460 461


462
    def rescanPeers(self):
463
        res = self.runRawSyncCommand(self.RESCAN_PEERS_COMMAND)
464
        psp = parseBirdShowProtocols(res)
465 466

        peers = []
467
        for pspl in psp[1]:
468
            if(re.match(self.proto_fltr,pspl[1])):
469
                peers.append(pspl[0])
470 471 472

        return peers

473 474 475 476 477 478
    def rescanRoutingTables(self):
        res = self.runRawSyncCommand(self.RESCAN_TABLES_COMMAND)

        tables = []
        for l in str.splitlines(res):
            m = bird_show_symbols_line_regexp.match(l)
Tomas Hlavacek's avatar
Tomas Hlavacek committed
479
            ulgmodel.debug("DEBUG TABLES match: "+m.group(2))
480 481
            if(m and m.group(2).lstrip().rstrip() == STRING_SYMBOL_ROUTING_TABLE):
                tables.append(m.group(1))
Tomas Hlavacek's avatar
Tomas Hlavacek committed
482 483

        ulgmodel.debug("DEBUG TABLES: "+str(tables))
484
        return tables
485

486
    def getBGPPeers(self):
487
        return self.rescanPeers()
488

489 490
    def getRoutingTables(self):
        return self.rescanRoutingTables()