libplab.py.in 56.3 KB
Newer Older
1
# -*- python -*-
Kirk Webb's avatar
Kirk Webb committed
2
3
#
# EMULAB-COPYRIGHT
4
# Copyright (c) 2000-2004 University of Utah and the Flux Group.
Kirk Webb's avatar
Kirk Webb committed
5
6
7
# All rights reserved.
#

8
"""
Kirk Webb's avatar
Kirk Webb committed
9
10
11
12
13
14
15
16
17
Library for interfacing with Plab.  This abstracts out the concepts of
Plab central, slices, and nodes.  All data (except static things like
certificates) is kept in the Emulab DB.  Unlike the regular dslice
svm, this one supports dynamically changing which nodes are in a
slice.

This requires an already obtained dslice certficate and key.  By
default it expects to find these in the @prefix@/etc/plab/
subdirectory.
18
19
20
"""

import sys
21
22
23
sys.path.append("@prefix@/lib")

import os, time
24
import string
Kirk Webb's avatar
   
Kirk Webb committed
25
import traceback
Kirk Webb's avatar
   
Kirk Webb committed
26
import signal
Kirk Webb's avatar
   
Kirk Webb committed
27
import socket
Kirk Webb's avatar
Kirk Webb committed
28
29
30
import httplib
import xml.parsers.expat
import re
Kirk Webb's avatar
   
Kirk Webb committed
31
import calendar
Kirk Webb's avatar
   
Kirk Webb committed
32
import shlex
Kirk Webb's avatar
   
Kirk Webb committed
33

Kirk Webb's avatar
   
Kirk Webb committed
34
from popen2 import Popen4
Kirk Webb's avatar
   
Kirk Webb committed
35
from warnings import warn
36

37
38
39
40
41
42
43
44
45
46
47
#
# Testbed and DB access libs
#
from libtestbed import *
from libdb import *

#
# Plab modules to import
#
from mod_PLC import mod_PLC
from mod_dslice import mod_dslice
Kirk Webb's avatar
   
Kirk Webb committed
48
from mod_PLCNM import mod_PLCNM
49
50

agents = {'PLC'    : mod_PLC,
Kirk Webb's avatar
   
Kirk Webb committed
51
52
          'dslice' : mod_dslice,
          'PLCNM'  : mod_PLCNM}
53

Kirk Webb's avatar
Kirk Webb committed
54
55
56
57
58
59
#
# output control vars
#
verbose = 0
debug = 0

60
61
62
#
# Constants
#
Kirk Webb's avatar
   
Kirk Webb committed
63
DEF_AGENT = "PLCNM";
64

65
RENEW_TIME = 2*24*60*60  # Renew two days before lease expires
Kirk Webb's avatar
   
Kirk Webb committed
66
67
68

RENEW_TIMEOUT = 1*60     # give the node manager a minute to respond to renew
FREE_TIMEOUT  = 1*60     # give the node manager a minute to respond to free
69
NODEPROBEINT  = 30
70

71
72
73
TBOPS = "@TBOPSEMAIL@".replace("\\","")
MAILTAG = "@THISHOMEBASE@"

74
RESERVED_PID = "emulab-ops"
75
RESERVED_EID = "hwdown"       # start life in hwdown
76
77
MONITOR_PID  = "emulab-ops"
MONITOR_EID  = "plab-monitor"
78

Kirk Webb's avatar
   
Kirk Webb committed
79
80
81
82
MAGIC_INET2_GATEWAYS = ("205.124.237.10",  "205.124.244.18", )
MAGIC_INET_GATEWAYS =  ("205.124.244.150", "205.124.239.185",
                        "205.124.244.154", "205.124.244.138",
                        "205.124.244.130", )
83
LOCAL_PLAB_DOMAIN = ".flux.utah.edu"
84
LOCAL_PLAB_LINKTYPE = "pcplabinet2"
Kirk Webb's avatar
   
Kirk Webb committed
85
86
87

# allowed nil/unknown values (sentinels).
ATTR_NIL_VALUES = ('None',)
88

89
90
91
92
# 'critical' node identifiers - those that are actually used to uniquely
# identify a planetlab node
ATTR_CRIT_KEYS = ('HNAME', 'IP', 'PLABID', 'MAC',)

93
94
95
96
# The amount by which latitude and longitude are allowed to differ before we
# classify them ask changed
LATLONG_DELTA = 0.001

Kirk Webb's avatar
   
Kirk Webb committed
97
PLABNODE = "@prefix@/sbin/plabnode"
98
SSH = "@prefix@/bin/sshtb"
99
NAMED_SETUP = "@prefix@/sbin/named_setup"
Kirk Webb's avatar
   
Kirk Webb committed
100
PELAB_PUSH  = "@prefix@/sbin/pelab_opspush"
101

Kirk Webb's avatar
Kirk Webb committed
102
103
104
105
ROOTBALL_URL = "http://localhost:1492/" # ensure this ends in a slash

DEF_PLAB_URL = "www.planet-lab.org"
DEF_SITE_XML = "/xml/sites.xml"
Kirk Webb's avatar
   
Kirk Webb committed
106
107
IGNORED_NODES_FILE = "@prefix@/etc/plab/IGNOREDNODES"
ALLOWED_NODES_FILE = "@prefix@/etc/plab/ALLOWEDNODES"
108

109
DEF_ROOTBALL_NAME = "@PLAB_ROOTBALL@"
110
SLICEPREFIX = "@PLAB_SLICEPREFIX@"
Kirk Webb's avatar
Kirk Webb committed
111
112
113
114
NODEPREFIX  = "plab"

BADSITECHARS = re.compile(r"\W+")
PLABBASEPRIO = 20000
Kirk Webb's avatar
   
Kirk Webb committed
115
PLAB_SVC_SLICENAME = "utah_svc_slice"
Kirk Webb's avatar
   
Kirk Webb committed
116
117
118
119
PLAB_SVC_SLICEDESC = "Emulab management service slice. Performs periodic " \
                     "checkins with Emulab central, and routes events for " \
                     "other Emulab slices. Slivers in this slice should " \
                     "only interact with other PlanetLab machines, and Emulab."
Kirk Webb's avatar
   
Kirk Webb committed
120
121
PLABMON_PID = "emulab-ops"
PLABMON_EID = "plab-monitor"
Kirk Webb's avatar
   
Kirk Webb committed
122
DEF_SLICE_DESC = "Slice created by Emulab"
Kirk Webb's avatar
   
Kirk Webb committed
123

Kirk Webb's avatar
   
Kirk Webb committed
124
125
PLABEXPIREWARN = 1*WEEK        # one week advance warning for slice expiration.
NODEEXPIREWARN = 2*WEEK+2*DAY  # about two weeks advance warning for slivers.
126

127
128
129
130
131
#
# var to track failed renewals
#
failedrenew = []

132
133
134
135
136
#
# Disable line buffering
#
sys.stdout = os.fdopen(sys.stdout.fileno(), sys.stdout.mode, 0)

Kirk Webb's avatar
   
Kirk Webb committed
137
138
139
140
141
#
# Ensure SIGPIPE doesn't bite us:
#
signal.signal(signal.SIGPIPE, signal.SIG_IGN)

142

143
144
145
#
# Plab abstraction
#
Kirk Webb's avatar
Kirk Webb committed
146

Kirk Webb's avatar
Kirk Webb committed
147
148
149
#
# Class responsible for parsing planetlab sites file
#
Kirk Webb's avatar
Kirk Webb committed
150
151
152
153
154
155
156
157
class siteParser:

    def __init__(self):
        self.parser = xml.parsers.expat.ParserCreate()
        self.parser.StartElementHandler = self.__site_start_elt
        self.parser.EndElementHandler = self.__site_end_elt
        self.__hosts = []
        self.__sitename = ""
158
159
        self.__latitude = 0
        self.__longitude = 0
Kirk Webb's avatar
Kirk Webb committed
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
        
    def getPlabNodeInfo(self):
                
        conn = httplib.HTTPSConnection(DEF_PLAB_URL)
        conn.request("GET", DEF_SITE_XML)
        res = conn.getresponse()
        if res.status != 200:
            raise RuntimeError, "HTTP Error getting site list:\n" \
                  "Code: %d Reason: %s" % \
                  (res.status, res.reason)
        try:
            self.parser.ParseFile(res)
            pass
        except xml.parsers.expat.ExpatError, e:
            print "Error parsing XML file, lineno: %d, offset: %d:\n%s" % \
                  (e.lineno, e.offset, xml.parsers.expat.ErrorString(e.code))
            raise

        return self.__hosts

    def __site_start_elt(self, name, attrs):
        
        if name == "PLANETLAB_SITES":
            pass
        
        elif name == "SITE":
            self.__sitename = attrs['SHORT_SITE_NAME']
187
188
189
190
191
192
193
194
            if attrs.has_key('LATITUDE'):
                self.__latitude = attrs['LATITUDE']
            else:
                self.__latiturde = 0
            if attrs.has_key('LONGITUDE'):
                self.__longitude = attrs['LONGITUDE']
            else:
                self.__longitude = 0
Kirk Webb's avatar
Kirk Webb committed
195
196
197
            pass
        
        elif name == "HOST":
Kirk Webb's avatar
   
Kirk Webb committed
198
199
200
            if not attrs.has_key('MAC'):
                attrs['MAC'] = "None"
                pass
201
202
203
204
205
206
207
            self.__hosts.append({'HNAME'     : attrs['NAME'],
                                 'IP'        : attrs['IP'],
                                 'PLABID'    : attrs['NODE_ID'],
                                 'MAC'       : attrs['MAC'],
                                 'SITE'      : self.__sitename,
                                 'LATITUDE'  : self.__latitude,
                                 'LONGITUDE' : self.__longitude})
Kirk Webb's avatar
Kirk Webb committed
208
209
210
211
212
213
214
215
216
217
218
219
            pass
        
        else:
            print "Unknown element in site file: %s: %s" % (name, attrs)
            pass
        
        return

    def __site_end_elt(self, name):
        
        if name == "SITE":
            self.__sitename = "Unknown"
220
221
            self.__latitude = 0
            self.__longitude = 0
Kirk Webb's avatar
Kirk Webb committed
222
223
224
225
            pass
        return

        
226
class Plab:
Kirk Webb's avatar
Kirk Webb committed
227
    def __init__(self, agent=None):
228
229
230
        if not agent:
            self.agent = agents[DEF_AGENT]()
            pass
Kirk Webb's avatar
Kirk Webb committed
231
        if debug:
232
233
234
            print "Using module: %s" % self.agent.modname
            pass
        pass
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251

    def createSlice(self, pid, eid):
        """
        Slice factory function
        """
        slice = Slice(self, pid, eid)
        slice._create()
        return slice

    def loadSlice(self, pid, eid):
        """
        Slice factory function
        """
        slice = Slice(self, pid, eid)
        slice._load()
        return slice

Kirk Webb's avatar
Kirk Webb committed
252
    def updateNodeEntries(self, ignorenew = False):
253
        """
Kirk Webb's avatar
Kirk Webb committed
254
        Finds out which Plab nodes are available, and
255
256
257
258
259
        update the DB accordingly.  If ignorenew is True, this will only
        make sure that the data in the DB is correct, and not complete.
        If ignorenew is False (the default), this will do a complete
        update of the DB.  However, this can take some time, as
        information about new nodes (such as link type) must be
Kirk Webb's avatar
Kirk Webb committed
260
        discovered.
261
262
263
264
265
266

        Note that this seemingly innocent funciton actually does a lot of
        magic.  This is the main/only way that Plab nodes get into the
        nodes DB, and this list is updated dynamically.  It also gathers
        static data about new nodes.
        """
Kirk Webb's avatar
Kirk Webb committed
267
268
269
        
        print "Getting available Plab nodes ..."

270
        avail = []
271
        try:
Kirk Webb's avatar
Kirk Webb committed
272
273
274
            parser = siteParser()
            avail = parser.getPlabNodeInfo()
            pass
275
        # XXX: rewrite to use more elegant exception info gathering.
276
277
        except:
            extype, exval, extrace = sys.exc_info()
278
            print "Error talking to agent: %s: %s" % (extype, exval)
Kirk Webb's avatar
Kirk Webb committed
279
            if debug:
280
281
282
                print extrace
            print "Going back to sleep until next scheduled poll"
            return
Kirk Webb's avatar
   
Kirk Webb committed
283

Kirk Webb's avatar
Kirk Webb committed
284
        if debug:
285
286
            print "Got advertisement list:"
            print avail
Kirk Webb's avatar
Kirk Webb committed
287
            pass
Kirk Webb's avatar
   
Kirk Webb committed
288

Kirk Webb's avatar
   
Kirk Webb committed
289
290
291
        ignored_nodes = self.__readNodeFile(IGNORED_NODES_FILE)
        allowed_nodes = self.__readNodeFile(ALLOWED_NODES_FILE)

Kirk Webb's avatar
   
Kirk Webb committed
292
293
294
295
296
297
        # Enforce node limitations, if any.
        # XXX: This is ugly - maybe move to a separate function
        #      that takes a list of filter functions.  I know!!
        #      Create a generator out of a set of filter functions
        #      and the initial node list! :-)  Python geek points to me if
        #      I ever get around to it...  KRW
Kirk Webb's avatar
   
Kirk Webb committed
298
        if len(allowed_nodes) or len(ignored_nodes):
Kirk Webb's avatar
   
Kirk Webb committed
299
            allowed = []
Kirk Webb's avatar
Kirk Webb committed
300
            for nodeent in avail:
Kirk Webb's avatar
   
Kirk Webb committed
301
                if nodeent['PLABID'] in ignored_nodes:
Kirk Webb's avatar
   
Kirk Webb committed
302
                    continue
Kirk Webb's avatar
   
Kirk Webb committed
303
304
                elif len(allowed_nodes):
                    if nodeent['IP'] in allowed_nodes:
Kirk Webb's avatar
   
Kirk Webb committed
305
306
307
308
309
                        allowed.append(nodeent)
                        pass
                    pass
                else:
                    allowed.append(nodeent)
Kirk Webb's avatar
Kirk Webb committed
310
311
                    pass
                pass
Kirk Webb's avatar
   
Kirk Webb committed
312
313
314
315
            if verbose:
                print "Advertisements in allowed nodes list:\n%s" % allowed
                pass
            avail = allowed
Kirk Webb's avatar
Kirk Webb committed
316
            pass
317

Kirk Webb's avatar
   
Kirk Webb committed
318
319
320
321
322
323
324
325
326
327
328
        # Check for duplicate node attributes (sanity check)
        availdups = self.__findDuplicateAttrs(avail)
        if len(availdups):
            SENDMAIL(TBOPS, "Duplicates in plab advertised node list",
                     "Duplicate attributes:\n"
                     "%s\n\n"
                     "Let plab support know!" % availdups,
                     TBOPS)
            raise RuntimeError, \
                  "Duplicate attributes in plab node listing:\n%s" % availdups

329
        # Get node info we already have.
330
        known = self.__getKnownPnodes()
Kirk Webb's avatar
Kirk Webb committed
331
        if debug:
332
333
            print "Got known pnodes:"
            print known
Kirk Webb's avatar
Kirk Webb committed
334
            pass
335

Kirk Webb's avatar
Kirk Webb committed
336
        # Create list of nodes to add or update
Kirk Webb's avatar
   
Kirk Webb committed
337
338
        toadd    = []  # List of node entries to add to DB
        toupdate = []  # List of node entries to update in the DB
Kirk Webb's avatar
Kirk Webb committed
339
        for nodeent in avail:
Kirk Webb's avatar
Kirk Webb committed
340
341
            # Replace sequences of bad chars in the site entity with
            # a single "-".
Kirk Webb's avatar
Kirk Webb committed
342
            nodeent['SITE'] = BADSITECHARS.sub("-", nodeent['SITE'])
Kirk Webb's avatar
   
Kirk Webb committed
343
344
345
            # Determine if we already know about this node.
            matchres = self.__matchPlabNode(nodeent, known)
            if not matchres:
Kirk Webb's avatar
   
Kirk Webb committed
346
                toadd.append(nodeent)
Kirk Webb's avatar
Kirk Webb committed
347
                pass
Kirk Webb's avatar
   
Kirk Webb committed
348
349
            elif len(matchres[1]):
                toupdate.append((nodeent,matchres))
Kirk Webb's avatar
Kirk Webb committed
350
                pass
Kirk Webb's avatar
   
Kirk Webb committed
351
            pass
Kirk Webb's avatar
Kirk Webb committed
352

Kirk Webb's avatar
   
Kirk Webb committed
353
354
        # Process the list of nodes to add
        addstr = ""
355
        if len(toadd):
Kirk Webb's avatar
Kirk Webb committed
356
            # Are we ignoring new entries?
357
            if ignorenew:
Kirk Webb's avatar
Kirk Webb committed
358
                if verbose:
359
                    print "%d new Plab nodes, but ignored for now" % len(toadd)
Kirk Webb's avatar
Kirk Webb committed
360
361
                    pass
                pass
Kirk Webb's avatar
Kirk Webb committed
362
            # If not ignoring, do the addition/update.
363
            else:
Kirk Webb's avatar
   
Kirk Webb committed
364
365
                print "There are %d new Plab nodes." % len(toadd)
                for nodeent in toadd:
Kirk Webb's avatar
Kirk Webb committed
366
                    # Get the linktype here so we can report it in email.
Kirk Webb's avatar
Kirk Webb committed
367
                    self.__findLinkType(nodeent)
Kirk Webb's avatar
Kirk Webb committed
368
                    if debug:
Kirk Webb's avatar
Kirk Webb committed
369
370
                        print "Found linktype %s for node %s" % \
                              (nodeent['LINKTYPE'], nodeent['IP'])
Kirk Webb's avatar
Kirk Webb committed
371
                        pass
Kirk Webb's avatar
   
Kirk Webb committed
372
373
374
                    # Add the node.
                    self.__addNode(nodeent)
                    # Add a line for the add/update message.
Kirk Webb's avatar
Kirk Webb committed
375
                    nodestr = "%s\t\t%s\t\t%s\t\t%s\t\t%s\n" % \
Kirk Webb's avatar
   
Kirk Webb committed
376
                              (nodeent['PLABID'],
Kirk Webb's avatar
Kirk Webb committed
377
378
379
380
                               nodeent['IP'],
                               nodeent['HNAME'],
                               nodeent['SITE'],
                               nodeent['LINKTYPE'])
Kirk Webb's avatar
   
Kirk Webb committed
381
                    addstr += nodestr
Kirk Webb's avatar
Kirk Webb committed
382
                    pass
Kirk Webb's avatar
   
Kirk Webb committed
383
384
                pass
            pass
385

Kirk Webb's avatar
   
Kirk Webb committed
386
        # Process node updates.
Kirk Webb's avatar
   
Kirk Webb committed
387
388
389
        updstr = ""
        if len(toupdate):
            print "There are %d plab node updates." % len(toupdate)
Kirk Webb's avatar
   
Kirk Webb committed
390
391
392
            for updent,updmapent in toupdate:
                self.__updateNodeMapping(updmapent)
                self.__addNode(updent, updmapent)
Kirk Webb's avatar
   
Kirk Webb committed
393
                # Add a line for the add/update message.
Kirk Webb's avatar
   
Kirk Webb committed
394
395
                nodestr = updmapent[0] + "\n"
                for attr,val in updmapent[1].items():
Kirk Webb's avatar
   
Kirk Webb committed
396
397
398
                    nodestr += "\t%s:\t%s => %s\n" % (attr,val[0],val[1])
                    pass
                updstr += nodestr + "\n"
Kirk Webb's avatar
Kirk Webb committed
399
400
                pass
            pass
Kirk Webb's avatar
   
Kirk Webb committed
401
402
403
404
405

        if len(toadd) or len(toupdate):
            # We need to update DNS since we've added hosts..
            print "Forcing a named map update ..."
            os.spawnl(os.P_WAIT, NAMED_SETUP, NAMED_SETUP)
Kirk Webb's avatar
   
Kirk Webb committed
406
407
            print "Pushing out site_mapping ..."
            os.spawnl(os.P_WAIT, PELAB_PUSH, PELAB_PUSH)
Kirk Webb's avatar
   
Kirk Webb committed
408
409
410
411
412
413
414
415
416
417
418
419
            # Now announce that we've added/updated nodes.
            SENDMAIL(TBOPS,
                     "Plab nodes have been added/updated in the DB.",
                     "The following plab nodes have been added to the DB:\n"
                     "PlabID\t\tIP\t\tHostname\t\tSite\t\tLinktype\n\n"
                     "%s\n\n"
                     "The following plab nodes have been updated in the DB:\n"
                     "\n%s\n\n" % \
                     (addstr, updstr),
                     TBOPS)
            print "Done adding new Plab nodes."
            pass
Kirk Webb's avatar
Kirk Webb committed
420
        return
421

Kirk Webb's avatar
   
Kirk Webb committed
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
    def __matchPlabNode(self, plabent, knownents):
        """
        Helper function.  Returns a two-element tuple or null.
        Null is returned when the node does not match any in the
        knownents list (none of it's attributes match those of any
        in the list).  If a match (or partial match) is found, a two
        element tuple is returned.  The first element is the emulab
        node id that matched, and the second is a dictionary containing
        thos elements that differed between the two (in the case of a
        partial match).
        """
        for nid in knownents:
            ent = knownents[nid]
            same = {}
            diff = {}
            for attr in ent:
                if ent[attr] in ATTR_NIL_VALUES:
                    continue
440
441
442
                elif (attr == "LATITUDE") or (attr == "LONGITUDE"):
                    # Special rules for latitude and longitude to avoid
                    # FP errors
443
444
445
446
447
448
                    if (ent[attr] != None and plabent[attr] != None) \
                           and (ent[attr] != "" and plabent[attr] != "") \
                           and ((float(ent[attr]) > \
                                 (float(plabent[attr]) + LATLONG_DELTA)) \
                                or (float(ent[attr]) < \
                                    (float(plabent[attr]) - LATLONG_DELTA))):
449
450
451
452
                        diff[attr] = (ent[attr], plabent[attr])
                    else:
                        same[attr] = ent[attr]
                        pass
Kirk Webb's avatar
   
Kirk Webb committed
453
454
455
456
457
458
459
                elif ent[attr] == plabent[attr]:
                    same[attr] = ent[attr]
                    pass
                else:
                    diff[attr] = (ent[attr], plabent[attr])
                    pass
                pass
460
461
            # Only consider these to be the same if at least one 'critical'
            # attr is the same
Kirk Webb's avatar
   
Kirk Webb committed
462
            if len(same):
463
464
465
                for attr in same:
                    if attr in ATTR_CRIT_KEYS:
                        return (nid, diff)
Kirk Webb's avatar
   
Kirk Webb committed
466
467
468
            pass
        return ()

469
470
471
472
473
    def __getKnownPnodes(self):
        """
        getFree helper function.  Returns a dict of IP:node_id pairs
        for the Plab nodes that currently exist in the DB.
        """
474
475
476
477
478
479
480
        res = DBQueryFatal("select plab_mapping.node_id,plab_id,"
                           "plab_mapping.hostname,IP,mac,site,latitude,"
                           "longitude"
                           " from plab_mapping"
                           " left join widearea_nodeinfo on"
                           "    plab_mapping.node_id = "
                           "    widearea_nodeinfo.node_id")
Kirk Webb's avatar
   
Kirk Webb committed
481
        
482
        ret = {}
483
484
485
486
487
488
489
490
        for nodeid, plabid, hostname, ip, mac, site, latitude, longitude in res:
            ret[nodeid] = {'PLABID'    : plabid,
                           'HNAME'     : hostname,
                           'IP'        : ip,
                           'MAC'       : mac,
                           'SITE'      : site,
                           'LATITUDE'  : latitude,
                           'LONGITUDE' : longitude}
Kirk Webb's avatar
Kirk Webb committed
491
            pass
Kirk Webb's avatar
   
Kirk Webb committed
492
493
494
495
496
497
498
499
500
501
        # Check for duplicate node attributes: report any that are found.
        dups = self.__findDuplicateAttrs(ret.values())
        if len(dups):
            SENDMAIL(TBOPS, "Duplicate plab node attributes in the DB!",
                     "Duplicate node attrs:\n"
                     "%s\n\n"
                     "Fix up please!" % dups,
                     TBOPS)
            raise RuntimeError, \
                  "Duplicate node attributes in DB:\n%s" % dups            
502
        return ret
Kirk Webb's avatar
   
Kirk Webb committed
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527

    def __findDuplicateAttrs(self, nodelist):
        """
        Find duplicate node attributes in the node list passed in.
        """
        uniqattrs = ['PLABID', 'HNAME', 'IP', 'MAC']
        attrs = {}
        dups = {}
        
        for ent in nodelist:
            for attr in uniqattrs:
                entry = "%s:%s" % (attr, ent[attr])
                if attrs.has_key(entry) and \
                   ent[attr] not in ATTR_NIL_VALUES:
                    print "Duplicate node attribute: %s" % entry
                    if not dups.has_key(entry):
                        dups[entry] = [attrs[entry],]
                        pass
                    dups[entry].append(ent['PLABID'])
                else:
                    attrs[entry] = ent['PLABID']
                    pass
                pass
            pass
        return dups
Kirk Webb's avatar
   
Kirk Webb committed
528
        
Kirk Webb's avatar
Kirk Webb committed
529
    def __findLinkType(self, nodeent):
530
531
532
533
534
535
536
        """
        getFree helper function.  Figures out the link type of the given
        host.  This first performs a traceroute and checks for the U of
        U's I2 gateway to classify Internet2 hosts.  If this test fails,
        it checks if the hostname is international.  If this test fails,
        this simply specifies an inet link type.

Kirk Webb's avatar
Kirk Webb committed
537
        This can't detect DSL links..
538
        """
539
        # Is host international (or flux/emulab local)?
540
        from socket import gethostbyaddr, getfqdn, herror
Kirk Webb's avatar
Kirk Webb committed
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
        
        if not nodeent.has_key('HNAME'):
            try:
                (hname, ) = gethostbyaddr(ip)
                nodeent['HNAME'] = getfqdn(hname)
                pass
            except herror:
                nodeent['HNAME'] = nodeent['IP']
                print "WARNING: Failed to get hostname for %s" % nodeent['IP']
                pass
            pass
        
        tld = nodeent['HNAME'].split(".")[-1].lower()
        if not tld in ("edu", "org", "net", "com", "gov", "us", "ca"):
            nodeent['LINKTYPE'] = "pcplabintl"
            return
        
        # Is it us?
        if nodeent['HNAME'].endswith(LOCAL_PLAB_DOMAIN):
            nodeent['LINKTYPE'] = LOCAL_PLAB_LINKTYPE
            return
        
563
        # Is host on I2?
Kirk Webb's avatar
Kirk Webb committed
564
        traceroute = os.popen("traceroute -nm 10 -q 1 %s" % nodeent['IP'])
565
566
567
568
569
        trace = traceroute.read()
        traceroute.close()

        for gw in MAGIC_INET2_GATEWAYS:
            if trace.find(gw) != -1:
Kirk Webb's avatar
Kirk Webb committed
570
571
                nodeent['LINKTYPE'] = "pcplabinet2"
                return
572

573
574
575
576
        for gw in MAGIC_INET_GATEWAYS:
            if trace.find(gw) != -1:
                break
        else:
Kirk Webb's avatar
Kirk Webb committed
577
            print "WARNING: Unknown gateway for host %s" % nodeent['IP']
578

579
        # Must be plain 'ole Internet
Kirk Webb's avatar
Kirk Webb committed
580
581
        nodeent['LINKTYPE'] = "pcplabinet"
        return
582

Kirk Webb's avatar
   
Kirk Webb committed
583
    def __addNode(self, nodeent, updent = ()):
584
585
586
587
588
        """
        getFree helper function.  Adds a new Plab pnode and associated
        vnodes to the DB.  linktype should be one of (inet2, inet, intl,
        dsl).
        """
Kirk Webb's avatar
   
Kirk Webb committed
589
590
        # block out common termination signals while adding a node
        osigs = disable_sigs(TERMSIGS)
591
        defosid, controliface = self.__getNodetypeInfo()
Kirk Webb's avatar
Kirk Webb committed
592
        hostonly = nodeent['HNAME'].replace(".", "-")
Kirk Webb's avatar
   
Kirk Webb committed
593
        # These will be setup properly below.
Kirk Webb's avatar
   
Kirk Webb committed
594
595
        nidnum = 0
        priority = 0
Kirk Webb's avatar
   
Kirk Webb committed
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
        nodeid = ""
        vnodeprefix = ""

        # Setup nodeid according to whether or not we were passed in an
        # update entry.
        if updent:
            nodeid = updent[0]
            print "Updating node %s" % nodeid
            pass
        else:
            nidnum, priority = self.__nextFreeNodeid()
            nodeid = "%s%d" % (NODEPREFIX, nidnum)
            vnodeprefix = "%svm%d" % (NODEPREFIX, nidnum)
            print "Creating pnode %s as %s, priority %d." % \
                  (nodeent['IP'], nodeid, priority)
            pass
612

613
614
        haslatlong = (('LATITUDE' in nodeent and 'LONGITUDE' in nodeent) and
            (nodeent['LATITUDE'] != 0 or nodeent['LONGITUDE'] != 0))
Kirk Webb's avatar
   
Kirk Webb committed
615
        try:
Kirk Webb's avatar
   
Kirk Webb committed
616
            DBQueryFatal("replace into widearea_nodeinfo"
617
618
619
620
621
622
623
                         " (node_id, contact_uid, hostname, site, latitude, "
                         "  longitude)"
                         " values (%s, %s, %s, %s, %s, %s)",
                         (nodeid, 'nobody', nodeent['HNAME'], nodeent['SITE'],
                          # Poor man's ternary operator
                          haslatlong and nodeent['LATITUDE'] or "NULL",
                          haslatlong and nodeent['LONGITUDE'] or "NULL"))
Kirk Webb's avatar
   
Kirk Webb committed
624
625
626
627
628
629
630
631
632
633
634
635
636

            DBQueryFatal("replace into interfaces"
                         " (node_id, card, port, IP, interface_type,"
                         " iface, role)"
                         " values (%s, %s, %s, %s, %s, %s, %s)",
                         (nodeid, 0, 1, nodeent['IP'], 'fxp',
                          controliface, 'ctrl'))

            # Don't do anything else if we are only updating the node
            if updent:
                enable_sigs(osigs)
                return

Kirk Webb's avatar
Kirk Webb committed
637
            DBQueryFatal("replace into nodes"
638
                         " (node_id, type, phys_nodeid, role, priority,"
Kirk Webb's avatar
   
Kirk Webb committed
639
                         "  op_mode, def_boot_osid,"
640
                         "  allocstate, allocstate_timestamp,"
Kirk Webb's avatar
   
Kirk Webb committed
641
                         "  eventstate, state_timestamp)"
642
                         " values (%s, %s, %s, %s, %s,"
Kirk Webb's avatar
   
Kirk Webb committed
643
644
645
646
                         "  %s, %s, %s, now(), %s, now())",
                         (nodeid, 'pcplabphys', nodeid,
                          'testnode', priority*100,
                          'ALWAYSUP', defosid,
647
                          'FREE_CLEAN',
Kirk Webb's avatar
   
Kirk Webb committed
648
                          'ISUP'))
649

650
651
652
            DBQueryFatal("replace into node_hostkeys"
                         " (node_id)"
                         " values (%s)",
Kirk Webb's avatar
Kirk Webb committed
653
                         (nodeid))
654

Kirk Webb's avatar
Kirk Webb committed
655
            DBQueryFatal("replace into reserved"
Kirk Webb's avatar
   
Kirk Webb committed
656
657
                         " (node_id, pid, eid, rsrv_time, vname)"
                         " values (%s, %s, %s, now(), %s)",
658
                         (nodeid, RESERVED_PID, RESERVED_EID, hostonly))
Kirk Webb's avatar
   
Kirk Webb committed
659

Kirk Webb's avatar
   
Kirk Webb committed
660
661
            # XXX: This should probably be checked and updated if necessary
            #      when updating.
Kirk Webb's avatar
Kirk Webb committed
662
            DBQueryFatal("replace into node_auxtypes"
Kirk Webb's avatar
   
Kirk Webb committed
663
664
                         " (node_id, type, count)"
                         " values (%s, %s, %s)",
Kirk Webb's avatar
Kirk Webb committed
665
                         (nodeid, nodeent['LINKTYPE'], 1))
Kirk Webb's avatar
   
Kirk Webb committed
666
            
Kirk Webb's avatar
Kirk Webb committed
667
            DBQueryFatal("replace into node_auxtypes"
668
669
670
671
                         " (node_id, type, count)"
                         " values (%s, %s, %s)",
                         (nodeid, 'pcplab', 1))
            
Kirk Webb's avatar
Kirk Webb committed
672
            DBQueryFatal("replace into node_status"
673
674
                         " (node_id, status, status_timestamp)"
                         " values (%s, %s, now())",
Kirk Webb's avatar
Kirk Webb committed
675
                         (nodeid, 'down'))
Kirk Webb's avatar
   
Kirk Webb committed
676

Kirk Webb's avatar
   
Kirk Webb committed
677
678
679
680
681
            DBQueryFatal("insert into plab_mapping"
                         " (node_id, plab_id, hostname, IP, mac, create_time)"
                         " values (%s, %s, %s, %s, %s, now())",
                         (nodeid, nodeent['PLABID'], nodeent['HNAME'],
                          nodeent['IP'], nodeent['MAC']))
682

Kirk Webb's avatar
   
Kirk Webb committed
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
            #
            # NowAdd the site_mapping entry for this node.
            #
            
            # See if we know about the associated site - grab idx if so
            siteidx = 0
            nodeidx = 1
            siteres = DBQueryFatal("select site_idx, node_idx from "
                                   " plab_site_mapping where site_name=%s",
                                   nodeent['SITE']);
            if len(siteres):
                # There are already nodes listed for this site, so get
                # the next node id.
                siteidx = siteres[0][0]
                for (foo, idx) in siteres:
                    if idx > nodeidx: nodeidx = idx
                    pass
                nodeidx += 1
                pass
            else:
                # No nodes listed for site, so get the largest site_idx
                # in the DB so far, and increment cuz we're going to add
                # a new one.
                maxres = DBQueryFatal("select MAX(site_idx) from "
                                      " plab_site_mapping")
                try:
                    siteidx = int(maxres[0][0]) + 1
                    pass
                except ValueError:
                    siteidx = 1
                    pass
                pass
            # Create site_mapping entry, optionally creating new site idx
            # via not specifying the site_idx field (field is auto_increment)
            DBQueryFatal("insert into plab_site_mapping "
                         " values (%s, %s, %s, %s)",
                         (nodeent['SITE'], siteidx, nodeid, nodeidx))

721
            # Create a single reserved plab vnode for the managment sliver.
Kirk Webb's avatar
   
Kirk Webb committed
722
723
724
725
726
727
728
729
            # XXX I left it as "20" cause of all the existing ones.
            # XXXX I set it to 1 due to the above comment (correct?)
            #      since we are re-creating anyway.            
            n = 1
            vprio = (priority * 100) + n
            sshdport = 38000 + n
            vnodeid = "%s-%d" % (vnodeprefix, n)
            vnodetype = "pcplab"
730
731
            if verbose:
                print "Creating vnode %s, priority %d" % (vnodeid, vprio)
Kirk Webb's avatar
Kirk Webb committed
732
                pass
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
                    
            DBQueryFatal("insert into nodes"
                         " (node_id, type, phys_nodeid, role, priority,"
                         "  op_mode, def_boot_osid, update_accounts,"
                         "  allocstate, allocstate_timestamp,"
                         "  eventstate, state_timestamp, sshdport)"
                         " values (%s, %s, %s, %s, %s,"
                         "  %s, %s, %s, %s, now(), %s, now(), %s)",
                         (vnodeid, vnodetype, nodeid, 'virtnode', vprio,
                          'PCVM', defosid, 1,
                          'FREE_CLEAN',
                          'SHUTDOWN', sshdport))

            DBQueryFatal("insert into node_hostkeys"
                         " (node_id)"
                         " values (%s)",
                         (vnodeid))
            
            DBQueryFatal("insert into node_status"
                         " (node_id, status, status_timestamp)"
                         " values (%s, %s, now())",
                         (vnodeid, 'up'))
            
Kirk Webb's avatar
Kirk Webb committed
756
            # Put the last vnode created into the special monitoring expt.
Kirk Webb's avatar
   
Kirk Webb committed
757
758
759
760
            DBQueryFatal("insert into reserved"
                         " (node_id, pid, eid, rsrv_time, vname)"
                         " values (%s, %s, %s, now(), %s)",
                         (vnodeid, MONITOR_PID, MONITOR_EID, vnodeid))
Kirk Webb's avatar
Kirk Webb committed
761
762
            pass
        
Kirk Webb's avatar
   
Kirk Webb committed
763
764
765
766
767
768
        except:
            print "Error adding PLAB node to DB: someone needs to clean up!"
            tbmsg = "".join(traceback.format_exception(*sys.exc_info()))
            SENDMAIL(TBOPS, "Error adding new plab node to DB: %s\n" %
                     nodeid, "Some operation failed while trying to add a"
                     " newly discovered plab node to the DB:\n %s"
769
                     "\n Please clean up!\n" % tbmsg, TBOPS)
Kirk Webb's avatar
   
Kirk Webb committed
770
771
            enable_sigs(osigs)
            raise
772

Kirk Webb's avatar
   
Kirk Webb committed
773
774
        # last but not least, unblock signals
        enable_sigs(osigs)
Kirk Webb's avatar
Kirk Webb committed
775
        return
776

Kirk Webb's avatar
   
Kirk Webb committed
777

Kirk Webb's avatar
   
Kirk Webb committed
778
    def __updateNodeMapping(self, updent):
Kirk Webb's avatar
   
Kirk Webb committed
779
780
781
782
783
        """
        Updates changed node attributes in the mapping table.
        """
        uid = os.getuid()
        dbuid = uid == 0 and "root" or UNIX2DBUID(uid)
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
        # mapping from attrs to which table they belong in
        tablemap = {'PLABID'    : 'plab_mapping',
                    'HNAME'     : 'plab_mapping',
                    'IP'        : 'plab_mapping',
                    'MAC'       : 'plab_mapping',
                    'SITE'      : 'widearea_nodeinfo',
                    'LATITUDE'  : 'widearea_nodeinfo',
                    'LONGITUDE' : 'widearea_nodeinfo',}
        # mapping from attrs to column names
        attrmap = {'plab_mapping' : {'PLABID' : 'plab_id',
                                     'HNAME'  : 'hostname',
                                     'IP'     : 'IP',
                                     'MAC'    : 'mac'},
                   'widearea_nodeinfo' : {'SITE'      : 'site',
                                          'LATITUDE'  : 'latitude',
                                          'LONGITUDE' : 'longitude',}}

Kirk Webb's avatar
   
Kirk Webb committed
801
802
803
804
805
806
807
808
809
810
811
812
813
        nodeid, chattrs = updent
        
        if len(chattrs) > 2:
            errmsg = "More than 2 plab node attrs have changed!\n\n%s\n\n" \
                     "%s has been moved to hwdown." % (chattrs, nodeid)
            MarkPhysNodeDown(nodeid)
            TBSetNodeLogEntry(nodeid, dbuid, TB_NODELOGTYPE_MISC, errmsg)
            SENDMAIL(TBOPS,
                     "More than 2 plab node attrs have changed on %s" % nodeid,
                     errmsg,
                     TBOPS)
            raise RuntimeError, errmsg # XXX: maybe don't raise an exception.
        
814
815
816
817
818
819
820
821
822
823
824
825
826
        # seperate out attrs by table
        chattrs_by_table = {}
        for attr in chattrs:
            table = tablemap[attr]
            if table not in chattrs_by_table:
                chattrs_by_table[table] = []
            chattrs_by_table[table].append(attr)

        # update each table
        for table in chattrs_by_table:
            updstr = ",".join(map(lambda x: "%s='%s'" %
                (attrmap[table][x[0]],x[1][1]), chattrs.items()))
            DBQueryFatal("update %s set %s where node_id='%s'" % (table, updstr, nodeid))
Kirk Webb's avatar
   
Kirk Webb committed
827
828
829
830
831
832
833
        updmsg = "Plab node %s attributes updated:\n\n%s" % (nodeid, chattrs)
        TBSetNodeLogEntry(nodeid, dbuid, TB_NODELOGTYPE_MISC, updmsg)
        # updateNodeEtries() already sends mail.
        #SENDMAIL(TBOPS,
        #         "Plab node %s attributes updated." % nodeid, updmsg, TBOPS)
        return

834
835
    def __getNodetypeInfo(self):
        """
836
837
        addNode helper function.  Returns a (defosid, controliface) 
        tuple for the Plab pnode type.  Caches the result since
838
        it doesn't change.
839
840
        """
        if not hasattr(self, "__getNodetypeInfoCache"):
Kirk Webb's avatar
Kirk Webb committed
841
            if debug:
842
                print "Getting node type info"
Kirk Webb's avatar
Kirk Webb committed
843
                pass
844
            res = DBQueryFatal("select osid, control_iface"
845
                               " from node_types"
846
847
848
                               " where type = 'pcplabphys'")
            assert (len(res) == 1), "Failed to get node type info"
            (self.__getNodetypeInfoCache, ) = res
Kirk Webb's avatar
Kirk Webb committed
849
850
            pass
        
851
852
853
854
855
856
857
        return self.__getNodetypeInfoCache

    def __nextFreeNodeid(self):
        """
        addNode helper function.  Returns a (nodeid, priority) tuple of
        the next free nodeid and priority for Plab nodes.
        """
Kirk Webb's avatar
Kirk Webb committed
858
        if debug:
859
860
861
862
863
864
865
866
867
868
            print "Getting next free nodeid"
        DBQueryFatal("lock tables nextfreenode write")
        try:
            res = DBQueryFatal("select nextid, nextpri from nextfreenode"
                               " where nodetype = 'pcplab'")
            assert (len(res) == 1), "Unable to find next free nodeid"
            DBQueryFatal("update nextfreenode"
                         " set nextid = nextid + 1, nextpri = nextpri + 1"
                         " where nodetype = 'pcplab'")
            ((nodeid, priority), ) = res
Kirk Webb's avatar
Kirk Webb committed
869
            pass
870
871
        finally:
            DBQueryFatal("unlock tables")
Kirk Webb's avatar
Kirk Webb committed
872
873
            pass
        
874
875
        return nodeid, priority

Kirk Webb's avatar
   
Kirk Webb committed
876
877
878
879
880
881
882
883
884
885
886
887
888
    def __readNodeFile(self, filename):
        """
        Helper function - read in list of nodes from a file, seperated
        by arbitrary amounts of whitespace.  No comments allowed.
        """
        nodelist = []
        if os.access(filename, os.F_OK):
            nodefile = open(filename, "r+")
            nodelist = nodefile.read().split()
            nodefile.close()
            pass
        return nodelist

Kirk Webb's avatar
   
Kirk Webb committed
889
    def renew(self, inpid = None, ineid = None, force = False):
890
        """
Kirk Webb's avatar
   
Kirk Webb committed
891
892
893
        Renews all of the Plab leases regardless of when they expire.  Note
        that all times are handled in the UTC time zone.  We don't trust
        MySQL to do the right thing with times (yet).
894
        """
Kirk Webb's avatar
   
Kirk Webb committed
895

Kirk Webb's avatar
   
Kirk Webb committed
896
897
        global failedrenew # XXX
        
Kirk Webb's avatar
   
Kirk Webb committed
898
899
900
901
902
903
904
905
906
907
        now = int(time.time())
        
        if not inpid:
            res = DBQueryFatal("select pid, eid from plab_slices");
            pass
        else:
            if not ineid:
                raise RuntimeError, "renew: Must provide eid with pid."
            res = ((inpid, ineid),)
            pass
Kirk Webb's avatar
Kirk Webb committed
908
        
909
        loadedSlices = {}
910
911
        newfail = []
        failsoon = []
Kirk Webb's avatar
Kirk Webb committed
912
913
        ret = 0

Kirk Webb's avatar
   
Kirk Webb committed
914
        print "Renewing Plab leases at %s ..." % time.ctime()
Kirk Webb's avatar
Kirk Webb committed
915

Kirk Webb's avatar
   
Kirk Webb committed
916
        for (pid, eid) in res:
Kirk Webb's avatar
Kirk Webb committed
917

918
919
            try:
                slice = loadedSlices[(pid, eid)]
Kirk Webb's avatar
Kirk Webb committed
920
                pass
921
922
923
            except KeyError:
                slice = self.loadSlice(pid, eid)
                loadedSlices[(pid, eid)] = slice
Kirk Webb's avatar
Kirk Webb committed
924
                pass
925
            
Kirk Webb's avatar
   
Kirk Webb committed
926
            res = slice.renew(force)
Kirk Webb's avatar
   
Kirk Webb committed
927
928
929
930
931
            entry = (pid, eid, slice.leaseend)
            
            if not res:
                print "Failed to renew lease for %s/%s" % \
                      entry[:2]
932
933
                if entry not in failedrenew:
                    newfail.append(entry)
Kirk Webb's avatar
Kirk Webb committed
934
                    pass
Kirk Webb's avatar
   
Kirk Webb committed
935
                if (slice.leaseend - now) < PLABEXPIREWARN:
936
                    failsoon.append(entry)
Kirk Webb's avatar
Kirk Webb committed
937
938
                    pass
                pass
939
940
941
            else:
                if entry in failedrenew:
                    failedrenew.remove(entry)
Kirk Webb's avatar
   
Kirk Webb committed
942
                    pass
Kirk Webb's avatar
Kirk Webb committed
943
                    
944
945
        if newfail:
            failedrenew += newfail
Kirk Webb's avatar
Kirk Webb committed
946
            failstr = ""
947
            for n in newfail:
Kirk Webb's avatar
   
Kirk Webb committed
948
                failstr += "%s/%s (expires: %s UTC)\n" % \
Kirk Webb's avatar
   
Kirk Webb committed
949
                           (n[:2] + (time.asctime(time.gmtime(n[2])),))
Kirk Webb's avatar
Kirk Webb committed
950
                pass
Kirk Webb's avatar
   
Kirk Webb committed
951
            
952
            SENDMAIL(TBOPS, "Lease renewal(s) failed",
Kirk Webb's avatar
   
Kirk Webb committed
953
                     "Failed to renew the following leases:\n%s" %
954
                     failstr + "\n\nPlease check the plabrenew log", TBOPS)
Kirk Webb's avatar
Kirk Webb committed
955
            pass
Kirk Webb's avatar
Kirk Webb committed
956

957
958
959
        if failsoon:
            failstr = ""
            for n in failsoon:
Kirk Webb's avatar
   
Kirk Webb committed
960
                failstr += "%s/%s: (expires: %s UTC)\n" % \
Kirk Webb's avatar
   
Kirk Webb committed
961
                           (n[:2] + (time.asctime(time.gmtime(n[2])),))
Kirk Webb's avatar
   
Kirk Webb committed
962
963
964
965
                pass
            SENDMAIL(TBOPS, "WARNING: PLAB leases have expired, or will soon",
                     "The following plab leases have expired, or will soon:\n"
                     + failstr + "\n\nPlease look into it!", TBOPS)
Kirk Webb's avatar
Kirk Webb committed
966
            pass
Kirk Webb's avatar
   
Kirk Webb committed
967

Kirk Webb's avatar
Kirk Webb committed
968
969
970
        return
    
    pass # end class Plab
971

972
973
974
975
976
977

#
# Slice abstraction
#

class Slice:
Kirk Webb's avatar
   
Kirk Webb committed
978
979

    def __init__(self, plab, pid, eid, slicename = None):
980
981
        self.plab = plab
        self.pid, self.eid = pid, eid
982
        self.slicemeta = None
Kirk Webb's avatar
   
Kirk Webb committed
983
        self.slicename = slicename
Kirk Webb's avatar
   
Kirk Webb committed
984
        self.description = DEF_SLICE_DESC
Kirk Webb's avatar
   
Kirk Webb committed
985
        return
986
987
988
989
990
991
    
    def _create(self):
        """
        Creates a new slice that initially contains no nodes.  Don't call
        this directly, use Plab.createSlice instead.
        """
Kirk Webb's avatar
   
Kirk Webb committed
992
993
994

        adminbit = 0
        if self.pid == PLABMON_PID and self.eid == PLABMON_EID:
Kirk Webb's avatar
   
Kirk Webb committed
995
996
            self.slicename   = PLAB_SVC_SLICENAME
            self.description = PLAB_SVC_SLICEDESC
Kirk Webb's avatar
   
Kirk Webb committed
997
998
999
            adminbit = 1
            pass
        
Kirk Webb's avatar
   
Kirk Webb committed
1000
        if not self.slicename:
For faster browsing, not all history is shown. View entire blame