ulg.py 22.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
#!/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
22
import os, sys
23 24
from genshi.template import TemplateLoader
from genshi.core import Markup
25 26 27
import cgi
import cgitb; cgitb.enable()
import pickle
28
import re
29
import fcntl
30
import traceback
31 32 33 34
import urllib
import md5
import time
import random
35

36
import config
37 38
import defaults

39
import ulgmodel
40

41 42 43
IPV4_ANNOTATE_REGEXP = '([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}(/[0-9]{1,2})?)'
ipv4_annotate_regexp = re.compile(IPV4_ANNOTATE_REGEXP)

44
### CGI output handler
45

46
class Session(object):
47 48 49 50 51 52 53 54 55 56 57
    def __init__(self,sessionid=None,routerid=None,commandid=None,parameters=[],result=None,finished=False,error=None,resrange=None,copy=None):
        if(copy):
            self.sessionid=copy.sessionid
            self.routerid=copy.routerid
            self.commandid=copy.commandid
            self.parameters=copy.parameters
            self.error=copy.error
            self.finished=copy.finished
            self.range=copy.range
            self.resultlen=copy.resultlen

58
        else:
59 60 61 62
            if(sessionid == None):
                self.sessionid = self.__genSessionId__()
            else:
                self.sessionid=sessionid
63

64 65 66 67 68 69 70
            self.routerid=routerid
            self.commandid=commandid
            self.parameters=parameters
            self.error=error
            self.finished=finished
            self.range=resrange
            self.resultlen=0
71

72
        self.save()
73

74 75
    def __genSessionId__(self):
        return md5.new(str(time.time())+str(random.randint(1,1000000))).hexdigest()
76

77 78 79 80
    @staticmethod
    def getSessionFileName(sessionid):
        if(re.compile('^[a-zA-Z0-9]{10,128}$').match(sessionid)):
            return defaults.session_dir+'/'+'ulg-'+sessionid+'.session'
81
        else:
82
            raise Exception('Invalid session id passed. Value was: '+sessionid)
83

84 85 86 87 88 89 90
    @staticmethod
    def getSessionOutputFileName(sessionid):
        if(re.compile('^[a-zA-Z0-9]{10,128}$').match(sessionid)):
            return defaults.session_dir+'/'+'ulg-'+sessionid+'.out.session'
        else:
            raise Exception('Invalid session id passed. Value was: '+sessionid)

91 92 93 94
    @staticmethod
    def load(sessionid):
        if(sessionid == None):
            return None
95

96 97
        try:
            fn = Session.getSessionFileName(sessionid)
98

99 100 101 102 103 104 105 106 107 108
            if(os.path.isfile(fn)):
                f = open(fn, 'rb')
                s = pickle.load(f)
                f.close()
                return s
            else:
                return None
            
        except Exception:
            return None
109

110 111 112 113 114 115
    def save(self):
        try:
            fn = Session.getSessionFileName(self.getSessionId())
            f = open(fn,'wb')
            pickle.dump(self, f)
            f.close()
116 117
        except:
            ulgmodel.log("Saving session failed: " + traceback.format_exc())
118

119 120
    def getSessionId(self):
        return self.sessionid
121

122 123 124
    def setFinished(self):
        self.finished=True
        self.save()
125

126 127
    def isFinished(self):
        return self.finished
128

129 130 131
    def setRouterId(self,router):
        self.routerid=routerid
        self.save()
132

133 134
    def getRouterId(self):
        return self.routerid
135

136 137 138
    def setCommandId(self,command):
        self.commandid = commandid
        self.save()
139

140 141 142 143 144 145
    def getCommandId(self):
        return self.commandid

    def cleanParameters(self):
        self.parameters = []
        self.save()
146

147 148 149 150 151
    def addParameter(self,parameter):
        if(not self.parameters):
            self.parameters = []
        self.parameters.append(parameter)
        self.save()
152

153 154
    def getParameters(self):
        return self.parameters
155

156
    def setResult(self,result):
157 158 159 160 161
        fn = Session.getSessionOutputFileName(self.sessionid)

        f = open(fn, 'w')
        f.write(result)
        f.close()
162

163
    def getResult(self):
164 165 166 167 168 169 170 171 172 173 174 175
        try:
            fn = Session.getSessionOutputFileName(self.sessionid)

            if(os.path.isfile(fn)):
                f = open(fn, 'r')
                result = f.read()
                f.close()
                return result
            else:
                return None
        except:
            return None
176

177
    def getDecoratedResult(self,decorator_helper,resrange=0,finished=False):
178
        if(self.getError()):
179
            # TODO
180 181
            return decorator_helper.pre(self.getResult())
        else:
182 183
            result = self.getResult()
            if(result):
184
                dr = self.getCommand().decorateResult(self,decorator_helper)
185 186
                self.resultlines = dr[1]
                return dr[0]
187 188 189
            else:
                return None

190
    def appendResult(self,result_fragment):
191 192 193 194 195 196 197 198 199 200 201 202 203
        fn = Session.getSessionOutputFileName(self.sessionid)

        f = open(fn, 'a')
        f.write(result_fragment)
        f.close()

    def clearResult(self):
        try:
            fn = Session.getSessionOutputFileName(self.sessionid)

            if(os.path.isfile(fn)):
                f=open(fn,'w')
                f.close()
204 205

            self.resultlines = 0
206 207
        except:
            pass
208 209 210 211

    def getRouter(self):
        if(self.getRouterId()!=None):
            return config.routers[self.getRouterId()]
212 213 214
        else:
            return None

215 216 217 218 219
    def getCommand(self):
        if(self.getRouterId()!=None)and(self.getCommandId()!=None):
            return self.getRouter().listCommands()[self.getCommandId()]
        else:
            return None
220

221 222 223 224 225
    def getError(self):
        return self.error

    def setError(self,error=None):
        self.error=error
226 227 228 229 230 231 232 233 234
        self.save()

    def getRange(self):
        return self.range

    def setRange(self,resrange):
        self.range=resrange
        self.save()

235 236 237
    def showRange(self):
        return self.getCommand().showRange()

238 239
    def getMaxRange(self):
        return self.resultlines
240

241

242
class DecoratorHelper:
243 244
    def __init__(self):
        pass
245

246 247
    def getScriptURL(self):
        return os.path.basename(__file__)
248

249
    def getURL(self,action,parameters={}):
250
        url=self.getScriptURL() + ("?action=%s" % urllib.quote(action))
251
        for k in parameters.keys():
252
            url = url + '&' + k + '=' + urllib.quote(parameters[k])
253
        return url
254

255 256
    def getIndexURL(self):
        return self.getURL('index')
257

258 259
    def getRuncommandURL(self,parameters={}):
        return self.getURL('runcommand',parameters)
260

261 262 263 264 265
    def getDisplayURL(self,sessionid,resrange=None):
        if(resrange):
            return self.getURL('display',{'sessionid':sessionid,'resrange':resrange})
        else:
            return self.getURL('display',{'sessionid':sessionid})
266

267 268 269 270 271 272
    def getDebugURL(self,parameters={}):
        return self.getURL('debug',parameters)

    def getErrorURL(self,parameters={}):
        return self.getURL('error',parameters)

273 274 275
    def getSpecialContentURL(self,sessionid,parameters={}):
        return self.getURL('getfile',dict({'sessionid':sessionid},**parameters))

276
    def getRouterID(self,router):
277 278 279 280
        for ridx,r in enumerate(config.routers):
            if(r == router):
                return ridx

281
        return 0
282

283
    def getCommandID(self,router,command):
284 285 286 287
        for cidx,c in enumerate(router.listCommands()):
            if(c == command):
                return cidx

288
        return 0
289

290 291 292 293
    def pre(self,text):
        return ('<pre>%s</pre>' % text)

    def ahref(self,url,text):
294
        return ('<a href=%s>%s</a>' % (str(url),str(text)))
295

296 297 298
    def copy_session(self,session):
        return Session(copy=session)

299 300 301 302 303 304
    def img(self,url,alternative_text=None):
        if(alternative_text):
            return ('<img src="%s" alt="%s">' % (url,alternative_text))
        else:
            return ('<img src="%s">' % url)

305 306 307 308 309
    def mwin(self,url,label=None):
        if(label == None):
            label = url
        return """<span style="cursor: pointer" onclick="TINY.box.show({iframe:'%s',boxid:'frameless',fixed:false,width:750,height:450,closejs:function(){closeJS()}})"><u>%s</u></span>""" % (url,label)

310 311
    def decorateASN(self,asn,prefix="AS"):
        return self.mwin(defaults.getASNURL(str(asn)),prefix+str(asn))
312

313
    def decoratePrefix(self,ip):
314 315
        return self.mwin(defaults.getIPPrefixURL(ip),ip)

316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
    def annotatePrefixes(self,line):
        s=0
        r=''
        for m in ipv4_annotate_regexp.finditer(line):
            r = r + line[s:m.start()]
            r = r + self.decoratePrefix(line[m.start():m.end()])
            s = m.end()
        return r + line[s:]

    def annotateIPs(self,line):
        s=0
        r=''
        for m in ipv4_annotate_regexp.finditer(line):
            r = r + line[s:m.start()]
            r = r + self.decoratePrefix(line[m.start():m.end()])
            s = m.end()
        return r + line[s:]
        

335

336
class ULGCgi:
337
    def __init__(self):
338 339 340 341
        self.loader=TemplateLoader(
            os.path.join(os.path.dirname(__file__), defaults.template_dir),
            auto_reload=True
            )
342

343
        self.decorator_helper = DecoratorHelper()
344

345 346 347 348 349

    def print_text_html(self):
        print "Content-Type: text/html\n"


350 351 352 353 354 355 356 357 358
    def increaseUsage(self):
        u = 0
        try:
            # Open files
            if(os.path.isfile(defaults.usage_counter_file)):
                lf = open(defaults.usage_counter_file,'r+')
                u = int(lf.readline())
            else:
                lf = open(defaults.usage_counter_file,'w')
359

360 361
            # Acquire lock
            fcntl.lockf(lf, fcntl.LOCK_EX)
362
        except (IOError,ValueError) as e:
363
            ulgmodel.log("Locking mechanism failure: "+str(e))
364
            return False
365

366 367 368 369 370 371 372 373 374 375 376 377
        try:
            if(u < defaults.usage_limit):
                # write a new value and release lock
                lf.seek(0)
                lf.write(str(u+1)+'\n')
                lf.close()
                return True
            else:
                # release lock
                lf.close()
                return False
        except IOError as e:
378
            ulgmodel.log("Locking mechanism update failure: "+str(e))
379
            return False
380

381 382 383 384 385 386 387 388 389
    def decreaseUsage(self):
        u = 1
        try:
            # Open files
            if(os.path.isfile(defaults.usage_counter_file)):
                lf = open(defaults.usage_counter_file,'r+')
                u = int(lf.readline())
            else:
                lf = open(defaults.usage_counter_file,'w')
390

391 392 393
            # Acquire lock
            fcntl.lockf(lf, fcntl.LOCK_EX)
        except IOError,ValueError:
394
            ulgmodel.log("Locking mechanism failure: "+str(e))
395
            return False
396

397 398 399 400 401 402 403 404 405 406 407 408
        try:
            if(u>0):
                # write a new value and release lock
                lf.seek(0)
                lf.write(str(u-1)+'\n')
                lf.close()
                return
            else:
                # release lock
                lf.close()
                return
        except IOError as e:
409
            ulgmodel.log("Locking mechanism update failure: "+str(e))
410 411 412 413 414 415
            pass

    def stopSessionOverlimit(self,session):
        session.setResult(defaults.STRING_SESSION_OVERLIMIT)
        session.setFinished()

416 417
    def getRefreshInterval(self,datalength=None):
        if(datalength):
418
            return (datalength/(1024*100))*defaults.refresh_interval + defaults.refresh_interval
419 420 421
        else:
            return defaults.refresh_interval

422 423 424 425 426 427 428 429 430 431 432 433
    def HTTPRedirect(self,url):
        return """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Redirect!</title>
<meta http-equiv="REFRESH" content="0;url=%s">
</head>
<body>
</body>
</html>""" % url

    def runCommand(self,session):
434 435 436 437 438 439 440
        class FakeSessionFile(object):
            def __init__(self,session):
                self.session = session

            def write(self,string):
                self.session.appendResult(string)

441 442 443 444 445 446 447 448 449 450 451 452 453
        # define trivial thread function
        def commandThreadBody(session,decreaseUsageMethod):
            ulgmodel.debug("Running command: "+session.getCommand().getName())
            try:
                session.getRouter().runAsyncCommand(session.getCommand(),session.getParameters(),FakeSessionFile(session))
            except Exception as e:
                ulgmodel.log("ERROR: Exception occured while running a command:" + traceback.format_exc())
                session.setResult("ERROR in commandThreadBody:\n"+traceback.format_exc())
            finally:
                ulgmodel.debug("Command finished: "+session.getCommand().getName())
                session.setFinished()
                decreaseUsageMethod()

454 455 456 457
        # try to increase usage counter
        if(self.increaseUsage()):
            # start new thread if needed
            if(defaults.always_start_thread or session.getRouter().getForkNeeded()):
458
                # fork a daemon process (fork two times to decouple with parent)
459 460 461 462 463 464 465 466 467 468 469 470 471
                sys.stdout.flush()
                child_pid = os.fork()
                if(child_pid == 0):
                    # detach process
                    devnull = open(os.devnull,'w')
                    os.dup2(devnull.fileno(),sys.stdout.fileno())
                    os.dup2(devnull.fileno(),sys.stderr.fileno())

                    # run the command
                    commandThreadBody(session,self.decreaseUsage)

                    # exit the child
                    sys.exit(0)
472

473
            else:
474
                # directly run the selected action, DEPRECATED
475 476 477 478
                commandThreadBody(session,self.decreaseUsage)
        else:
            # stop and report no-op
            self.stopSessionOverlimit(session)
479 480


481 482
    def renderULGIndex(self,routerid=0,commandid=0,sessionid=None):
        template = self.loader.load(defaults.index_template_file)
483

484 485 486 487 488
        return template.generate(defaults=defaults,
                                 routers=config.routers,
                                 default_routerid=routerid,
                                 default_commandid=commandid,
                                 default_sessionid=sessionid,
489
                                 getFormURL=self.decorator_helper.getRuncommandURL
490
                                 ).render('html', doctype='html')
491 492


493 494 495
    def renderULGAction(self,routerid=0,commandid=0,sessionid=None,**moreparams):
        routerid=int(routerid)
        commandid=int(commandid)
496

497 498
        # create and register session
        session = Session(sessionid=sessionid,routerid=routerid,commandid=commandid)
499
        session.clearResult()
500 501 502 503 504
    
        # extract parameters
        session.cleanParameters()
        for pidx,ps in enumerate(session.getCommand().getParamSpecs()):
            if('param'+str(pidx) in moreparams.keys()):
505
                session.addParameter(str(moreparams['param'+str(pidx)]))
506 507
            else:
                session.addParameter(ps.getDefault())
508

509
        # run the command (possibly in a separate process)
510
        self.runCommand(session)
511

512
        # redirect to the session display
513
        return self.HTTPRedirect(self.decorator_helper.getDisplayURL(session.getSessionId()))
514 515


516 517
    def renderULGResult(self,sessionid=None,resrange=0):
        def getRangeStepURLs(session,decorator_helper):
518 519 520
            if(not session.showRange()):
                return None

521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549
            cur_range = session.getRange()
            max_range = session.getMaxRange()

            if(max_range < defaults.range_step):
                return None

            res = []
            if((cur_range - defaults.range_step * 100) >= 0):
                res.append(('<<',decorator_helper.getDisplayURL(session.getSessionId(),str(cur_range - defaults.range_step * 100))))
            if((cur_range - defaults.range_step * 10) >= 0):
                res.append(('<',decorator_helper.getDisplayURL(session.getSessionId(),str(cur_range - defaults.range_step * 10))))

            boundary_size = 8
            leftb = max(0,cur_range - boundary_size*defaults.range_step)
            rightb = min(leftb + 2*boundary_size*defaults.range_step,max_range)

            for rb in range(leftb,rightb,defaults.range_step):
                if(rb != cur_range):
                    res.append((str(rb),decorator_helper.getDisplayURL(session.getSessionId(),str(rb))))
                else:
                    res.append((str(rb),None))

            if((cur_range + defaults.range_step * 10) < max_range):
                res.append(('>',decorator_helper.getDisplayURL(session.getSessionId(),str(cur_range + defaults.range_step * 10))))
            if((cur_range + defaults.range_step * 100) < max_range):
                res.append(('>>',decorator_helper.getDisplayURL(session.getSessionId(),str(cur_range + defaults.range_step * 100))))

            return res

550
        if(sessionid==None):
551
            return self.HTTPRedirect(self.decorator_helper.getErrorURL())
552

553 554
        session = Session.load(sessionid)
        if(session == None):
555
            return self.HTTPRedirect(self.decorator_helper.getErrorURL())
556

557 558
        session.setRange(int(resrange))

559
        result_text = session.getDecoratedResult(self.decorator_helper,session.getRange(),session.isFinished())
560

561 562
        if(session.isFinished()):
            refresh=None
563
        else:
564 565 566 567
            if(result_text):
                refresh = self.getRefreshInterval(len(result_text))
            else:
                refresh = self.getRefreshInterval()
568 569 570 571 572 573 574 575

        template = self.loader.load(defaults.index_template_file)
        return template.generate(defaults=defaults,
                                 routers=config.routers,
                                 default_routerid=session.getRouterId(),
                                 default_commandid=session.getCommandId(),
                                 default_params=session.getParameters(),
                                 default_sessionid=sessionid,
576
                                 result=Markup(result_text) if(result_text) else None,
577
                                 refresh=refresh,
578 579 580
                                 getFormURL=self.decorator_helper.getRuncommandURL,
                                 resrange=str(session.getRange()),
                                 resrangeb=getRangeStepURLs(session,self.decorator_helper),
581 582 583 584 585 586 587 588 589
                                 ).render('html', doctype='html')

    def renderULGError(self,sessionid=None,**params):
        template = self.loader.load(defaults.index_template_file)

        result_text = defaults.STRING_ARBITRARY_ERROR

        session = Session.load(sessionid)
        if(session!=None):
590
            result_text=self.decorator_helper.pre(self.sessions[sessionid].getError())
591 592 593 594 595 596

        return template.generate(defaults=defaults,
                                 routers=config.routers,
                                 default_routerid=0,
                                 default_commandid=0,
                                 default_sessionid=None,
597
                                 result=Markup(result_text) if(result_text) else None,
598
                                 refresh=0,
599
                                 getFormURL=self.decorator_helper.getRuncommandURL
600 601 602 603 604 605 606 607 608 609 610 611 612 613 614
                                 ).render('html', doctype='html')
    
    def renderULGDebug(self,**params):
        template = self.loader.load(defaults.index_template_file)

        result_text = "<h1>DEBUG</h1>\n<pre>\nPARAMS:\n"
        for k in params.keys():
            result_text = result_text + str(k) + "=" + str(params[k]) + "\n"
        result_test = result_text + "\n<pre>"

        return template.generate(defaults=defaults,
                                 routers=config.routers,
                                 default_routerid=0,
                                 default_commandid=0,
                                 default_sessionid=None,
615
                                 result=Markup(result_text) if(result_text) else None,
616
                                 refresh=0,
617
                                 getFormURL=self.decorator_helper.getRuncommandURL()
618 619
                                 ).render('html', doctype='html')

620 621 622 623 624 625 626 627 628

    def getULGSpecialContent(self,sessionid,**params):
        if(sessionid==None):
            return self.HTTPRedirect(self.decorator_helper.getErrorURL())

        session = Session.load(sessionid)
        if(session == None):
            return self.HTTPRedirect(self.decorator_helper.getErrorURL())

629 630
        # speciality here: the function is responsible for printing the output itself
        session.getCommand().getSpecialContent(session,**params)
631 632


633
    def index(self, **params):
634
        self.print_text_html()
635 636
        if('sessionid' in params.keys()):
            print self.renderULGIndex(sessionid=params['sessionid'])
637
        else:
638
            print self.renderULGIndex()
639

640
    def runcommand(self,routerid=0,commandid=0,sessionid=None,**params):
641
        self.print_text_html()
642
        print self.renderULGAction(routerid,commandid,sessionid,**params)
643

644
    def display(self,sessionid=None,**params):
645
        self.print_text_html()
646
        print self.renderULGResult(sessionid,**params)
647

648
    def getfile(self,sessionid=None,**params):
649
        self.getULGSpecialContent(sessionid,**params)
650

651
    def error(self,sessionid=None,**params):
652
        self.print_text_html()
653
        print self.renderULGError(sessionid,**params)
654

655
    def debug(self,**params):
656
        self.print_text_html()
657
        print self.renderULGDebug(**params)
658

659
# main
660

661
if __name__=="__main__":
662 663 664
    try:
        form = cgi.FieldStorage()
        handler = ULGCgi()
665

666 667
        action = form.getvalue('action',None)
        params = dict([(k,form.getvalue(k)) for k in form.keys() if k != 'action'])
668
    
669 670 671 672 673 674 675
        if(action):
            if(action == 'index'):
                handler.index(**params)
            elif(action == 'runcommand'):
                handler.runcommand(**params)
            elif(action == 'display'):
                handler.display(**params)
676 677
            elif(action == 'getfile'):
                handler.getfile(**params)
678
            elif(action == 'error'):
679
                handler.error(**params)
680 681 682 683 684 685 686
            elif(action == 'debug'):
                handler.debug(**params)
            else:
                ulgmodel.log('ERROR: Unknown action called: '+action+'\n')
                handler.display(**params)

        else:
687
            handler.index(**params)
688 689
    except Exception as e:
        ulgmodel.log("ERROR in CGI: "+traceback.format_exc())