ulgbird.py 18 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]+)'
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

54
BIRD_SH_ROUTE_ALL_ASES_REGEXP = "^(\s*BGP\.as_path:\s+)([0-9\s]+)\s*$"
Tomas Hlavacek's avatar
Tomas Hlavacek committed
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
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 = []
70
    params = dict(DEFAULT_PARAMS)
Tomas Hlavacek's avatar
Tomas Hlavacek committed
71 72 73 74 75 76 77 78 79
    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):
80
            ases = ["AS"+str(asn) for asn in [prependas] + split_ases(m.group(2))]
Tomas Hlavacek's avatar
Tomas Hlavacek committed
81
            res.append((ases,params))
82
            params = dict(DEFAULT_PARAMS)
Tomas Hlavacek's avatar
Tomas Hlavacek committed
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
            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.decorateASN(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.decorateIP(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

311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
    def decorateResult(self,session,decorator_helper=None):
        def decorateLine(l):			
            m = bird_sh_route_all_ases_regexp.match(l)
            if(m):
                r = m.group(1)
                ases = str.split(m.group(2))
                for asn in ases:
                    r = r + decorator_helper.decorateASN(asn,prefix='')
                    r = r + ' '
                return r
            else:
                return l


        s = str.splitlines(session.getResult())
        r=''
        for sl in s:
            r += decorateLine(sl) + "\n"
            
        return ("<pre>\n%s\n</pre>" % r, len(s))


Tomas Hlavacek's avatar
Tomas Hlavacek committed
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
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

359

360
class BirdRouterLocal(ulgmodel.LocalRouter):
361
    RESCAN_PEERS_COMMAND = 'show protocols'
362
    RESCAN_TABLES_COMMAND = 'show symbols'
363
    DEFAULT_PROTOCOL_FLTR = '^(Kernel|Device|Static|BGP).*$'
364

Tomas Hlavacek's avatar
Tomas Hlavacek committed
365
    def __init__(self,sock=defaults.default_bird_sock,commands=None,proto_fltr=None,asn='My ASN'):
366
        super(self.__class__,self).__init__()
367 368
        self.sock = sock
        self.setName('localhost')
Tomas Hlavacek's avatar
Tomas Hlavacek committed
369
        self.setASN(asn)
370 371 372 373
        if(proto_fltr):
            self.proto_fltr = proto_fltr
        else:
            self.proto_fltr = self.DEFAULT_PROTOCOL_FLTR
374

375
        # command autoconfiguration might run only after other parameters are set
376 377 378
        if(commands):
            self.setCommands(commands)
        else:
379 380 381
            self.setCommands(self._getDefaultCommands())

    def _getDefaultCommands(self):
382
        sh_proto_all = BirdShowProtocolsAllCommand(self.getBGPPeers())
383
        sh_proto_route = BirdShowRouteProtocolCommand(self.getBGPPeers(),self.getRoutingTables())
384 385
        sh_proto_export = BirdShowRouteExportCommand(self.getBGPPeers())
        return [BirdShowProtocolsCommand(show_proto_all_command=sh_proto_all, proto_filter = self.proto_fltr),
386
                BirdShowRouteCommand(self.getRoutingTables()),
387 388 389
                sh_proto_all,
                sh_proto_route,
                sh_proto_export,
390
                BirdShowRouteAllCommand(self.getRoutingTables()),
Tomas Hlavacek's avatar
Tomas Hlavacek committed
391
                BirdGraphShowRouteAll(self.getRoutingTables()),
392 393
                ulgmodel.TextCommand('show status'),
                ulgmodel.TextCommand('show memory')
394
                ]
395

396
    def runRawCommand(self,command,outfile):
397 398 399 400 401
        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))
402

403 404 405 406
            em = bird_sock_reply_end_regexp.match(line)
            if(em):
                # most likely the last line of the reply
                return (int(em.group(1)),None)
407

408 409 410
            if(line[0] == '+'):
                # ignore async reply
                return (None,None)
411

412 413 414
            if(line[0] == ' '):
                # return reply line as it is (remove padding)
                return (None,line[1:])
415

416
            raise Exception("Can not parse BIRD output line: "+line)
417

418 419
        def isBirdSockReplyEnd(code):
            if(code==None):
420 421
                return False

422 423 424 425 426
            if(code == 0):
                # end of reply
                return True
            elif(code == 13):
                # show status last line
427
                return True
428 429 430
            elif(code >= 9000):
                # probably error
                return True
431 432 433
            else:
                return False

434 435 436 437 438
#        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)
439

440 441
        # cretate FD for the socket
        sf=s.makefile()
442

443 444
        # wait for initial header
        l = sf.readline()
445

446 447 448
        # send the command string
        sf.write(command+"\n")
        sf.flush()
449

450 451 452
        # read and capture lines until the output delimiter string is hit
        while(True):
            l = sf.readline()
453

454
            ulgmodel.debug("Raw line read: " + l)
455

456 457 458 459 460
            # 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]))
461

462 463
                if(lp[1]):
                    ulgmodel.debug("Last read line after normalize: " + lp[1])
464
                    outfile.write(lp[1].rstrip()+"\n")
465 466 467 468
                break
            else:
                if(lp[1]):
                    ulgmodel.debug("Read line after normalize: " + lp[1])
469
                    outfile.write(lp[1].rstrip()+"\n")
470
                else:
471
                    ulgmodel.debug("Read line was empty after normalize.")
472

473 474
        # close the socket and return captured result
        s.close()
475

476 477 478
#        except socket.timeout as e:
#            # catch only timeout exception, while letting other exceptions pass
#            outfile.result(defaults.STRING_SOCKET_TIMEOUT)
479 480 481

    def getForkNeeded(self):
        return False
482 483


484
    def rescanPeers(self):
485
        res = self.runRawSyncCommand(self.RESCAN_PEERS_COMMAND)
486
        psp = parseBirdShowProtocols(res)
487 488

        peers = []
489
        for pspl in psp[1]:
490
            if(re.match(self.proto_fltr,pspl[1])):
491
                peers.append(pspl[0])
492 493 494

        return peers

495 496 497 498 499 500 501 502
    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)
            if(m and m.group(2).lstrip().rstrip() == STRING_SYMBOL_ROUTING_TABLE):
                tables.append(m.group(1))
Tomas Hlavacek's avatar
Tomas Hlavacek committed
503

504
        return tables
505

506
    def getBGPPeers(self):
507
        return self.rescanPeers()
508

509 510
    def getRoutingTables(self):
        return self.rescanRoutingTables()