libplab.py.in 58.1 KB
Newer Older
1
# -*- python -*-
Kirk Webb's avatar
Kirk Webb committed
2
3
#
# EMULAB-COPYRIGHT
4
# Copyright (c) 2000-2004, 2006 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

Kirk Webb's avatar
   
Kirk Webb committed
71
72
USERNODE = "@USERNODE@"
TBOPS = "@TBOPSEMAIL_NOSLASH@"
73
MAILTAG = "@THISHOMEBASE@"
Kirk Webb's avatar
   
Kirk Webb committed
74
SLICE_ALIAS_DIR = "/etc/mail/plab-slice-addrs"
75

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

Kirk Webb's avatar
   
Kirk Webb committed
81
82
83
84
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", )
85
LOCAL_PLAB_DOMAIN = ".flux.utah.edu"
86
LOCAL_PLAB_LINKTYPE = "pcplabinet2"
Kirk Webb's avatar
   
Kirk Webb committed
87
88
89

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

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

95
96
97
98
# 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
99
PLABNODE = "@prefix@/sbin/plabnode"
100
SSH = "@prefix@/bin/sshtb"
101
NAMED_SETUP = "@prefix@/sbin/named_setup"
Kirk Webb's avatar
   
Kirk Webb committed
102
PELAB_PUSH  = "@prefix@/sbin/pelab_opspush"
103

Kirk Webb's avatar
Kirk Webb committed
104
105
106
107
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
108
109
IGNORED_NODES_FILE = "@prefix@/etc/plab/IGNOREDNODES"
ALLOWED_NODES_FILE = "@prefix@/etc/plab/ALLOWEDNODES"
110

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

BADSITECHARS = re.compile(r"\W+")
PLABBASEPRIO = 20000
Kirk Webb's avatar
   
Kirk Webb committed
117
PLAB_SVC_SLICENAME = "utah_svc_slice"
Kirk Webb's avatar
   
Kirk Webb committed
118
119
120
121
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
122
123
PLABMON_PID = "emulab-ops"
PLABMON_EID = "plab-monitor"
Kirk Webb's avatar
   
Kirk Webb committed
124
DEF_SLICE_DESC = "Slice created by Emulab"
Kirk Webb's avatar
   
Kirk Webb committed
125

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

129
130
131
132
133
#
# var to track failed renewals
#
failedrenew = []

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

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

144

145
146
147
#
# Plab abstraction
#
Kirk Webb's avatar
Kirk Webb committed
148

Kirk Webb's avatar
Kirk Webb committed
149
150
151
#
# Class responsible for parsing planetlab sites file
#
Kirk Webb's avatar
Kirk Webb committed
152
153
154
155
156
157
158
159
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 = ""
160
161
        self.__latitude = 0
        self.__longitude = 0
Kirk Webb's avatar
Kirk Webb committed
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
187
188
        
    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']
189
190
191
192
193
194
195
196
            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
197
198
199
            pass
        
        elif name == "HOST":
Kirk Webb's avatar
   
Kirk Webb committed
200
201
202
            if not attrs.has_key('MAC'):
                attrs['MAC'] = "None"
                pass
203
204
205
206
207
208
209
            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
210
211
212
213
214
215
216
217
218
219
220
221
            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"
222
223
            self.__latitude = 0
            self.__longitude = 0
Kirk Webb's avatar
Kirk Webb committed
224
225
226
227
            pass
        return

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

    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
254
    def updateNodeEntries(self, ignorenew = False):
255
        """
Kirk Webb's avatar
Kirk Webb committed
256
        Finds out which Plab nodes are available, and
257
258
259
260
261
        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
262
        discovered.
263
264
265
266
267
268

        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
269
270
271
        
        print "Getting available Plab nodes ..."

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

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

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

Kirk Webb's avatar
   
Kirk Webb committed
294
295
296
297
298
299
        # 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
300
        if len(allowed_nodes) or len(ignored_nodes):
Kirk Webb's avatar
   
Kirk Webb committed
301
            allowed = []
Kirk Webb's avatar
Kirk Webb committed
302
            for nodeent in avail:
Kirk Webb's avatar
   
Kirk Webb committed
303
                if nodeent['PLABID'] in ignored_nodes:
Kirk Webb's avatar
   
Kirk Webb committed
304
                    continue
Kirk Webb's avatar
   
Kirk Webb committed
305
306
                elif len(allowed_nodes):
                    if nodeent['IP'] in allowed_nodes:
Kirk Webb's avatar
   
Kirk Webb committed
307
308
309
310
311
                        allowed.append(nodeent)
                        pass
                    pass
                else:
                    allowed.append(nodeent)
Kirk Webb's avatar
Kirk Webb committed
312
313
                    pass
                pass
Kirk Webb's avatar
   
Kirk Webb committed
314
315
316
317
            if verbose:
                print "Advertisements in allowed nodes list:\n%s" % allowed
                pass
            avail = allowed
Kirk Webb's avatar
Kirk Webb committed
318
            pass
319

Kirk Webb's avatar
   
Kirk Webb committed
320
321
322
323
324
325
326
327
328
329
330
        # 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

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

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

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

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

        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
408
409
            print "Pushing out site_mapping ..."
            os.spawnl(os.P_WAIT, PELAB_PUSH, PELAB_PUSH)
Kirk Webb's avatar
   
Kirk Webb committed
410
411
412
413
414
415
416
417
418
419
420
421
            # 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
422
        return
423

Kirk Webb's avatar
   
Kirk Webb committed
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
    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
442
443
444
                elif (attr == "LATITUDE") or (attr == "LONGITUDE"):
                    # Special rules for latitude and longitude to avoid
                    # FP errors
445
446
447
448
449
450
                    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))):
451
452
453
454
                        diff[attr] = (ent[attr], plabent[attr])
                    else:
                        same[attr] = ent[attr]
                        pass
Kirk Webb's avatar
   
Kirk Webb committed
455
456
457
458
459
460
461
                elif ent[attr] == plabent[attr]:
                    same[attr] = ent[attr]
                    pass
                else:
                    diff[attr] = (ent[attr], plabent[attr])
                    pass
                pass
462
463
            # Only consider these to be the same if at least one 'critical'
            # attr is the same
Kirk Webb's avatar
   
Kirk Webb committed
464
            if len(same):
465
466
467
                for attr in same:
                    if attr in ATTR_CRIT_KEYS:
                        return (nid, diff)
Kirk Webb's avatar
   
Kirk Webb committed
468
469
470
            pass
        return ()

471
472
473
474
475
    def __getKnownPnodes(self):
        """
        getFree helper function.  Returns a dict of IP:node_id pairs
        for the Plab nodes that currently exist in the DB.
        """
476
477
478
479
480
481
482
        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
483
        
484
        ret = {}
485
486
487
488
489
490
491
492
        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
493
            pass
Kirk Webb's avatar
   
Kirk Webb committed
494
495
496
497
498
499
500
501
502
503
        # 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            
504
        return ret
Kirk Webb's avatar
   
Kirk Webb committed
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529

    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
530
        
Kirk Webb's avatar
Kirk Webb committed
531
    def __findLinkType(self, nodeent):
532
533
534
535
536
537
538
        """
        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
539
        This can't detect DSL links..
540
        """
541
        # Is host international (or flux/emulab local)?
542
        from socket import gethostbyaddr, getfqdn, herror
Kirk Webb's avatar
Kirk Webb committed
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
        
        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
        
565
        # Is host on I2?
Kirk Webb's avatar
Kirk Webb committed
566
        traceroute = os.popen("traceroute -nm 10 -q 1 %s" % nodeent['IP'])
567
568
569
570
571
        trace = traceroute.read()
        traceroute.close()

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

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

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

Kirk Webb's avatar
   
Kirk Webb committed
585
    def __addNode(self, nodeent, updent = ()):
586
587
588
589
590
        """
        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
591
592
        # block out common termination signals while adding a node
        osigs = disable_sigs(TERMSIGS)
593
        defosid, controliface = self.__getNodetypeInfo()
Kirk Webb's avatar
Kirk Webb committed
594
        hostonly = nodeent['HNAME'].replace(".", "-")
Kirk Webb's avatar
   
Kirk Webb committed
595
        # These will be setup properly below.
Kirk Webb's avatar
   
Kirk Webb committed
596
597
        nidnum = 0
        priority = 0
Kirk Webb's avatar
   
Kirk Webb committed
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
        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
614

615
616
        haslatlong = (('LATITUDE' in nodeent and 'LONGITUDE' in nodeent) and
            (nodeent['LATITUDE'] != 0 or nodeent['LONGITUDE'] != 0))
Kirk Webb's avatar
   
Kirk Webb committed
617
        try:
Kirk Webb's avatar
   
Kirk Webb committed
618
            DBQueryFatal("replace into widearea_nodeinfo"
619
620
621
622
623
624
625
                         " (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
626
627
628
629
630

            DBQueryFatal("replace into interfaces"
                         " (node_id, card, port, IP, interface_type,"
                         " iface, role)"
                         " values (%s, %s, %s, %s, %s, %s, %s)",
Kirk Webb's avatar
   
Kirk Webb committed
631
                         (nodeid, 0, 1, nodeent['IP'], 'plab_fake',
Kirk Webb's avatar
   
Kirk Webb committed
632
633
634
635
636
637
638
                          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
639
            DBQueryFatal("replace into nodes"
640
                         " (node_id, type, phys_nodeid, role, priority,"
Kirk Webb's avatar
   
Kirk Webb committed
641
                         "  op_mode, def_boot_osid,"
642
                         "  allocstate, allocstate_timestamp,"
Kirk Webb's avatar
   
Kirk Webb committed
643
                         "  eventstate, state_timestamp)"
644
                         " values (%s, %s, %s, %s, %s,"
Kirk Webb's avatar
   
Kirk Webb committed
645
646
647
648
                         "  %s, %s, %s, now(), %s, now())",
                         (nodeid, 'pcplabphys', nodeid,
                          'testnode', priority*100,
                          'ALWAYSUP', defosid,
649
                          'FREE_CLEAN',
Kirk Webb's avatar
   
Kirk Webb committed
650
                          'ISUP'))
651

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

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

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

Kirk Webb's avatar
   
Kirk Webb committed
679
680
681
682
683
            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']))
684

Kirk Webb's avatar
   
Kirk Webb committed
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
721
722
            #
            # 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))

723
            # Create a single reserved plab vnode for the managment sliver.
Kirk Webb's avatar
   
Kirk Webb committed
724
725
726
727
728
729
730
731
            # 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"
732
733
            if verbose:
                print "Creating vnode %s, priority %d" % (vnodeid, vprio)
Kirk Webb's avatar
Kirk Webb committed
734
                pass
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
                    
            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
758
            # Put the last vnode created into the special monitoring expt.
Kirk Webb's avatar
   
Kirk Webb committed
759
760
761
762
            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
763
764
            pass
        
Kirk Webb's avatar
   
Kirk Webb committed
765
766
767
768
769
770
        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"
771
                     "\n Please clean up!\n" % tbmsg, TBOPS)
Kirk Webb's avatar
   
Kirk Webb committed
772
773
            enable_sigs(osigs)
            raise
774

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

Kirk Webb's avatar
   
Kirk Webb committed
779

Kirk Webb's avatar
   
Kirk Webb committed
780
    def __updateNodeMapping(self, updent):
Kirk Webb's avatar
   
Kirk Webb committed
781
782
783
784
785
        """
        Updates changed node attributes in the mapping table.
        """
        uid = os.getuid()
        dbuid = uid == 0 and "root" or UNIX2DBUID(uid)
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
        # 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
803
804
805
806
807
808
809
810
811
812
813
814
815
        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.
        
816
817
818
819
820
821
822
823
824
825
826
827
828
        # 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
829
830
831
832
833
834
835
        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

836
837
    def __getNodetypeInfo(self):
        """
838
839
        addNode helper function.  Returns a (defosid, controliface) 
        tuple for the Plab pnode type.  Caches the result since
840
        it doesn't change.
841
842
        """
        if not hasattr(self, "__getNodetypeInfoCache"):
Kirk Webb's avatar
Kirk Webb committed
843
            if debug:
844
                print "Getting node type info"
Kirk Webb's avatar
Kirk Webb committed
845
                pass
846
847
848
849
850
851
852
853
854
855

            dbres = DBQueryFatal("select attrkey,attrvalue "
                                 " from node_type_attributes as a "
                                 " where type = 'pcplabphys' and "
                                 "       (a.attrkey='default_osid' or "
                                 "        a.attrkey='control_interface') "
                                 " order by attrkey")
            
            assert (len(dbres) == 2), "Failed to get node type info"
            res = (dbres[0][1], dbres[1][1])
856
            (self.__getNodetypeInfoCache, ) = res
Kirk Webb's avatar
Kirk Webb committed
857
858
            pass
        
859
860
861
862
863
864
865
        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
866
        if debug:
867
868
869
870
871
872
873
874
875
876
            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
877
            pass
878
879
        finally:
            DBQueryFatal("unlock tables")
Kirk Webb's avatar
Kirk Webb committed
880
881
            pass
        
882
883
        return nodeid, priority

Kirk Webb's avatar
   
Kirk Webb committed
884
885
886
887
888
889
890
891
892
893
894
895
896
    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
897
    def renew(self, inpid = None, ineid = None, force = False):
898
        """
Kirk Webb's avatar
   
Kirk Webb committed
899
900
901
        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).
902
        """
Kirk Webb's avatar
   
Kirk Webb committed
903

Kirk Webb's avatar
   
Kirk Webb committed
904
905
        global failedrenew # XXX
        
Kirk Webb's avatar
   
Kirk Webb committed
906
907
908
909
910
911
912
913
914
915
        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
916
        
917
        loadedSlices = {}
918
919
        newfail = []
        failsoon = []
Kirk Webb's avatar
Kirk Webb committed
920
921
        ret = 0

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

Kirk Webb's avatar
   
Kirk Webb committed
924
        for (pid, eid) in res:
Kirk Webb's avatar
Kirk Webb committed
925

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

965
966
967
        if failsoon:
            failstr = ""
            for n in failsoon:
Kirk Webb's avatar
   
Kirk Webb committed
968
                failstr += "%s/%s: (expires: %s UTC)\n" % \
Kirk Webb's avatar
   
Kirk Webb committed
969
                           (n[:2] + (time.asctime(time.gmtime(n[2])),))
Kirk Webb's avatar
   
Kirk Webb committed
970
971
972
973
                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
974
            pass
Kirk Webb's avatar
   
Kirk Webb committed
975

Kirk Webb's avatar
Kirk Webb committed
976
977
978
        return
    
    pass # end class Plab
979

980
981
982
983
984
985

#
# Slice abstraction
#

class Slice:
Kirk Webb's avatar
   
Kirk Webb committed
986
987

    def __init__(self, plab, pid, eid, slicename = None):
988
989
        self.plab = plab
        self.pid, self.eid = pid, eid
990
        self.slicemeta = None
Kirk Webb's avatar
   
Kirk Webb committed
991
        self.slicename = slicename
Kirk Webb's avatar
   
Kirk Webb committed
992
        self.description = DEF_SLICE_DESC
Kirk Webb's avatar
   
Kirk Webb committed
993
        return
994
995
996
997
998
999
    
    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
1000