mod_PLC4.py.in 45.2 KB
Newer Older
1 2
# -*- python -*-
#
3
# Copyright (c) 2000-2003, 2007-2008 University of Utah and the Flux Group.
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# 
# {{{EMULAB-LICENSE
# 
# This file is part of the Emulab network testbed software.
# 
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
# 
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
# License for more details.
# 
# You should have received a copy of the GNU Affero General Public License
# along with this file.  If not, see <http://www.gnu.org/licenses/>.
# 
# }}}
23 24 25 26 27 28 29 30 31 32 33 34
#

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

import xmlrpclib
import getopt
import fcntl
import time
import calendar
import cPickle
import os
35
import socket
36

37
import libdb
38 39 40 41
from libtestbed import *
from aspects import wrap_around
from timer_advisories import timeAdvice

42
import popen2
43
import random
44

45 46 47 48 49 50 51
#
# output control vars
#
verbose = 0
debug = 0

#
52
# Constants
53
#
54
DEF_NM_PORT = "812"
55 56 57 58

#
# A bunch of time constants / intervals (in seconds)
#
59
MAX_PLC_LEASELEN = 2*MONTH-5*DAY   # defined by PLC as ~two months (56 days)
60 61
                                   # but have to use 55 now cause PLC won't
                                   # take 56.
62
MIN_LEASE_WINDOW = 2*MONTH-12*DAY  # minimum time until expiration
63 64 65 66 67 68
MAX_LEASE_SLOP = 600 # (ten minutes)
MAX_CACHE_TIME = HOUR # (one hour)

DEF_SLICE_DESC = "Slice created by Emulab"
DEF_EMULAB_URL = "http://www.emulab.net"

69
INSTMETHOD_NMCONTROLLER = "nm-controller"
70 71 72 73
INSTMETHOD_DELEGATED = "delegated"
INSTMETHOD_PLCINST = "plc-instantiated"
DEF_EMULAB_INSTMETHOD = INSTMETHOD_DELEGATED

74 75 76
DEF_NM_SSHCMD = "/usr/bin/ssh -q -o StrictHostKeyChecking=no" \
                " -o PasswordAuthentication=no -o NumberOfPasswordPrompts=0" \
                " -l %s -i %s %s"
77 78

class NM4agent:
79
    def __init__(self,IP,nodeid,del_acct,del_key,nmport=DEF_NM_PORT):
80 81 82 83 84 85 86 87 88 89 90 91
        # Instead of ssh xmlrpc transport, we use xmlrpclib load/dumps.
        self._isopen = False
        self.delacct = del_acct
        self.delkey = del_key
        
        self.__vers = [4,0,0]
        self.IP = IP
        self.nodeid = nodeid
        pass

    def _open(self):
        if not self._isopen:
92 93 94 95
            if debug:
                print "connecting to NM over ssh with command '%s'" % \
                      (DEF_NM_SSHCMD % (self.delacct,self.delkey,self.IP),)
                pass
96 97 98
            try:
                self.__agentconn = popen2.Popen3(DEF_NM_SSHCMD % (self.delacct,
                                                                  self.delkey,
99 100
                                                                  self.IP),
                                                 capturestderr=True)
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
                self._isopen = True
            except:
                raise
            pass
        pass

    def _close(self):
        if self._isopen:
            # Nothing else we can do except wait for the connection to die,
            # and that's silly... or kill the pid ourself---but the connection
            # will naturally die after the response.
            self._isopen = False
        pass

    def _xcall(self,cmd,args=()):
116
        
117
        self._open()
118 119
        try:
            if debug:
120 121
                print "NM4: xmlrpc send(%s/%s): %s%s" % (self.nodeid,self.IP,
                                                         cmd,str(args))
122 123 124
                pass
            print >>self.__agentconn.tochild, xmlrpclib.dumps(args,cmd)
            self.__agentconn.tochild.close()
125
            retval = self.__agentconn.fromchild.read()
126 127 128 129 130
            if debug:
                print "NM4: xmlrpc raw response(%s/%s): '%s'" % (self.nodeid,
                                                                 self.IP,
                                                                 str(retval))
                pass
131 132 133
            #if retval == None or retval == '':
            #    raise RuntimeError("no response from NM")
            retval = xmlrpclib.loads(retval)
134
            if debug:
135 136 137
                print "NM4: xmlrpc response(%s/%s): '%s'" % (self.nodeid,
                                                             self.IP,
                                                             str(retval))
138 139 140
                pass
            self.__agentconn.wait()
            if debug:
141
                print "NM4: xmlrpc done(%s/%s)" % (self.nodeid,self.IP)
142 143 144
                pass
            self._close()
        except:
145 146 147 148 149 150 151
            if self.__agentconn.poll() == -1:
                # Try waiting for it...
                self.__agentconn.wait()
                pass

            if self.__agentconn.poll() == 65280:
                self._close()
152 153
                raise RuntimeError, "Error: SSH failure to %s/%s" % \
                      (self.nodeid,self.IP)
154 155 156 157
            else:
                # always close; we can only send one xmlrpc request at a time
                self._close()
                if debug:
158
                    print "ssh returned %s" % str(self.__agentconn.poll())
159 160
                    pass
                raise
161
                #raise RuntimeError, "Error: xmlrpc call failed!"
162
            pass
163
        
164 165
        # XXX: we whack the retval to be compat with NMv3
        rret = retval[0][0]
166
        if not rret == 0:
167 168
            rret = 0
            pass
169 170 171
        else:
            rret = 1
            pass
172 173 174 175 176 177
        retval = [ rret, [''] ]
        
        return retval
    
    def deliver_ticket(self,ticket):
        if debug:
178
            print "NM4: deliver_ticket(%s/%s)" % (self.nodeid,self.IP)
179
            pass
180 181
        return self._xcall('Ticket',(ticket,))
    
182
    def create_sliver(self,slice_name,tries=2,interval=5):
183
        if debug:
184 185
            print "NM4: create_sliver(%s/%s), slice %s" % (self.nodeid,self.IP,
                                                           slice_name)
186 187 188 189 190 191
            pass
        (success,rtries) = (False,tries)
        while not success and rtries > 0:
            rtries -= 1
            try:
                retval = self._xcall('Create',(slice_name,))
192
                retval = self._xcall('Start',(slice_name,))
193 194 195 196 197
                success = True
            except:
                if rtries == 0:
                    raise
                else:
198 199
                    print "NM4: create_sliver(%s/%s) failed" \
                          % (self.nodeid,self.IP)
200 201 202 203
                    time.sleep(interval)
                    pass
                pass
            pass
204 205
        # munge the retval for compat with old NMagent
        return (retval,'')
206
    
207 208
    def delete_sliver(self,slice_name):
        if debug:
209 210
            print "NM4: delete_sliver(%s/%s), slice %s" % (self.nodeid,self.IP,
                                                           slice_name)
211 212 213
        return self._xcall('Destroy',(slice_name,))

    def start_sliver(self,slice_name):
214 215 216
        if debug:
            print "NM4: start_sliver(%s/%s), slice %s" % (self.nodeid,self.IP,
                                                          slice_name)
217 218 219
        return self._xcall('Start',(slice_name,))

    def stop_sliver(self,slice_name):
220 221 222
        if debug:
            print "NM4: stop_sliver(%s/%s), slice %s" % (self.nodeid,self.IP,
                                                         slice_name)
223 224 225 226 227
        return self._xcall('Stop',(slice_name,))

    # NM v4 does not have a version method...
    def version(self):
        return self.__vers
228 229 230 231 232 233 234 235 236 237 238 239 240 241

    def ping(self):
        try:
            # No better way to ping v4 than to call Help.
            res = self._xcall('Help')
            if res[0] == 0:
                return True
            pass
        except:
            if debug:
                traceback.print_exc()
                pass
            pass
        return False
242 243 244 245 246
    
    def getAgentClass(self):
        return self.__class__
    pass

247 248 249 250 251
#
# The real PLC agent.  Wraps up standard arguments to the
# PLC XMLRPC interface.
#
class PLCagent:
252 253 254
    def __init__(self, plc, slicename):
        self.slicename = slicename
        self.auth = plc.getAuthParameter()
255 256
        
        try:
257 258
            # Need allow_none=1 to marshal `None' since PLC4 uses it
            # in wildcard situations...
259
            self.__server = xmlrpclib.ServerProxy(plc.url,allow_none=1)
260 261 262 263 264 265
        except:
            print "Failed to create XML-RPC proxy"
            raise
        return

    def getSliceName(self):
266
        return self.slicename
267 268 269 270 271

    def SliceCreate(self,
                    sliceurl = DEF_EMULAB_URL,
                    slicedesc = DEF_SLICE_DESC,
                    instmethod = DEF_EMULAB_INSTMETHOD):
272
        return self.__server.AddSlice(self.auth,
273 274
                                      { 'url' : sliceurl,
                                        'instantiation' : instmethod,
275
                                        'name' : self.slicename,
276 277 278
                                        'description' : slicedesc })

    def SliceDelete(self):
279
        return self.__server.DeleteSlice(self.auth, self.slicename)
280 281

    def SliceUpdate(self,slicedesc = DEF_SLICE_DESC,sliceURL = DEF_EMULAB_URL):
282
        return self.__server.UpdateSlice(self.auth, self.slicename,
283 284 285 286
                                         { 'url' : sliceURL,
                                           'description' : slicedesc })

    def SliceRenew(self,expdate):
287
        return self.__server.UpdateSlice(self.auth, self.slicename,
288 289 290 291 292 293
                                         { 'expires' : expdate })

    def SliceNodesAdd(self,nodelist):
        if not type(nodelist) == list:
            nodelist = [nodelist]
            pass
294
        return self.__server.AddSliceToNodes(self.auth,self.slicename,
295 296 297 298 299
                                             nodelist)
    
    def SliceNodesDel(self,nodelist):
        if not type(nodelist) == list:
            nodelist = [nodelist,]
300
        return self.__server.DeleteSliceFromNodes(self.auth,self.slicename,
301 302 303 304 305
                                                  nodelist)

    # Ick, have to use GetSlices and GetNodes.  Ick!  Ick!
    def SliceNodesList(self):
        # use a return filter to get only the node ids.
306
        retval = self.__server.GetSlices(self.auth,[ self.slicename ])
307 308 309
        # well, there's a bug in PLC with return filters in GetSlices...
        #                                 [ 'node_ids' ])

310
        if len(retval) > 0 and retval[0]['name'] == self.slicename:
311 312 313 314
            nidlist = retval[0]['node_ids']
            # then use a node filter to GetNodes to get the names.
            # XXX: probably should convert over to using node ids at
            # some point.
315
            retval = self.__server.GetNodes(self.auth,nidlist,[ 'hostname' ])
316 317 318 319
        else:
            retval = []
        
        nhostlist = map(lambda x: x['hostname'], retval)
320 321 322 323 324 325 326 327 328 329 330 331 332

        return nhostlist

    # PLC 4's default behavior is to add one user at a time.  Thus, we
    # just call their function for each list item; if a fault occurs, it'll
    # go back to the caller.  The reason we can do it this way is because
    # subsequent calls to AddPersonToSlice don't fail if the person is already
    # a member of the slice.  It's nasty, but whatever.
    def SliceUsersAdd(self, userlist):
        if type(userlist) != list:
            userlist = [userlist]
            pass
        for user in userlist:
333 334
            retval = self.__server.AddPersonToSlice(self.auth,user,
                                                    self.slicename)
335 336 337 338 339 340 341 342 343 344
            pass
        # this is 1 if all (i.e., the last) call succeeds
        return retval

    # Same PLC 4 behavior change as for AddPersonToSlice.
    def SliceUsersDel(self, userlist):
        if type(userlist) != list:
            userlist = [userlist]
            pass
        for user in userlist:
345 346
            retval = self.__server.DeletePersonFromSlice(self.auth,user,
                                                         self.slicename)
347 348 349 350 351 352
            pass
        return retval

    # Ick, have to implement this in terms of GetSlices and GetPersons
    # just as for the node hostnames in SliceNodesList.
    def SliceUsersList(self):
353
        retval = self.__server.GetSlices(self.auth,[ self.slicename ])
354 355
        # XXX PLC bug
        #                                 [ 'name','person_ids' ])
356
        if len(retval) > 0 and retval[0]['name'] == self.slicename:
357
            uidlist = retval[0]['person_ids']
358
            retval = self.__server.GetPersons(self.auth,uidlist,[ 'email' ])
359 360 361 362
        else:
            retval = []
        
        usernamelist = map(lambda x: x['email'], retval)
363 364 365 366

        return usernamelist
    
    def SliceGetTicket(self):
367
        return self.__server.GetSliceTicket(self.auth,self.slicename)
368

369 370 371 372 373 374
    def SliceInfo(self,infilter=None,outfilter=None):
        return self.__server.GetSlices(self.auth,infilter,outfilter)

    def NodeInfo(self,infilter=None,outfilter=None):
        return self.__server.GetNodes(self.auth,infilter,outfilter)

375 376 377
    def NodeGroupInfo(self,infilter=None,outfilter=None):
        return self.__server.GetNodeGroups(self.auth,infilter,outfilter)

378 379
    def PersonInfo(self,infilter=None,outfilter=None):
        return self.__server.GetPersons(self.auth,infilter,outfilter)
380 381 382
    
    def KeyInfo(self,infilter=None,outfilter=None):
        return self.__server.GetKeys(self.auth,infilter,outfilter)
383 384 385 386 387 388

    def NodeNetworkInfo(self,infilter=None,outfilter=None):
        return self.__server.GetNodeNetworks(self.auth,infilter,outfilter)

    def SiteInfo(self,infilter=None,outfilter=None):
        return self.__server.GetSites(self.auth,infilter,outfilter)
389 390

    def AuthCheck(self):
391
        return self.__server.AuthCheck(self.auth)
392

393
    def SliceAddAttribute(self,attrname,attrvalue,node=None):
394 395 396
        if attrname == None or attrvalue == None \
           or attrname == '' or attrvalue == '':
            return 0
397 398 399 400 401 402 403
        if node:
            return self.__server.AddSliceAttribute(self.auth,self.slicename,
                                                   attrname,attrvalue,node)
        else:
            return self.__server.AddSliceAttribute(self.auth,self.slicename,
                                                   attrname,attrvalue)
        pass
404

405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
    def SiteAdd(self,name,url,longitude,latitude):
        return self.__server.AddSite(self.auth,
                                     { 'name':name,'login_base':name,
                                       'abbreviated_name':name,'url':url,
                                       'longitude':longitude,
                                       'latitude':latitude,
                                       'enabled':True,'is_public':False,
                                       'max_slices':20,'max_slivers':1000 })

    def SiteUpdate(self,id,url,longitude,latitude):
        return self.__server.UpdateSite(self.auth,id,
                                        { 'url':url,
                                          'longitude':longitude,
                                          'latitude':latitude })

    def SiteDelete(self,id):
        return self.__server.DeleteSite(self.auth,id)

423 424 425 426 427 428
    def SiteId(self,id):
        retval = self.SiteInfo(id,['site_id'])
        if retval and 'site_id' in retval:
            return retval['site_id']
        return None

429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
    def __keygen(self):
        astr = "abcdefghijklmnopqrstuvwxyz"
        astr = astr + astr.upper() + '0123456789'
        tpasslist = random.sample(astr,32)
        tkey = ''
        for char in tpasslist:
            tkey += char
            pass
        return tkey

    def NodeAdd(self,site,hostname):
        nid = self.__server.AddNode(self.auth,site,{ 'hostname':hostname })
        self.__server.UpdateNode(self.auth,nid,{ 'key':self.__keygen() })
        return nid

    def NodeUpdate(self,id,hostname,networks):
        nni = self.NodeNetworkInfo(infilter={ 'node_id':id })
        # we just ensure that any network in networks is bound to this node;
        # we leave any other bound ones alone.
        _all_check_keys = [ 'broadcast','is_primary','network','ip','dns1',
                            'dns2','hostname','netmask','gateway','mac',
                            'bwlimit','type','method' ]
        _most_check_keys = [ 'hostname','ip','mac','netmask' ]
        # find the primary node network -- if we have a primary net that
        # doesn't match, must remove this one first.
        primary_nn = None
        for ni in nni:
            if ni['is_primary']:
                primary_nn = ni
                break
            pass
        # check the existing networks for _most_check_keys; if at least two
        # of these match, we have a match and may need to update that nnid.
        for ni in networks:
            found = None
            for exni in nni:
                if exni.has_key('__done'):
                    continue
                match_count = 0
                for k in _most_check_keys:
                    if ni.has_key(k) and ni[k] == exni[k]:
                        match_count += 1
                        pass
                    pass
                if match_count > 1:
                    found = exni
                    exni['__done'] = True
                    break
                pass

            if not found:
                ad = {}
                for k in _all_check_keys:
                    if ni.has_key(k):
                        ad[k] = ni[k]
                        pass
                    pass
                # if we are adding the primary network, delete the old primary
                if ad.has_key('is_primary') and ad['is_primary'] \
                       and primary_nn:
                    self.__server.DeleteNodeNetwork(self.auth,
                                                    primary_nn['nodenetwork_id'])
                    pass

                self.__server.AddNodeNetwork(self.auth,id,ad)
                pass
            else:
                # see if we need to do an update:
                update = False
                ad = {}
                for k in _all_check_keys:
                    if ni.has_key(k) and ni[k] != exni[k]:
                        ad[k] = ni[k]
                        pass
                    pass
                if len(ad.keys()) > 0:
505 506
                    self.__server.UpdateNodeNetwork(self.auth,
                                                    exni['nodenetwork_id'],ad)
507 508 509 510 511 512 513 514 515 516 517 518 519
                    pass
                pass
            pass

        if hostname:
            self.__server.UpdateNode(self.auth,id,{ 'hostname':hostname })
            pass

        return

    def NodeDelete(self,id):
        return self.__server.DeleteNode(self.auth,id)

520 521 522 523 524 525
    def NodeId(self,id):
        retval = self.NodeInfo(id,['node_id'])
        if retval and 'node_id' in retval:
            return retval['node_id']
        return None

526 527 528 529
    def NodeSetBootState(self,id,boot_state):
            self.__server.UpdateNode(self.auth,id,{ 'boot_state':boot_state })
            pass

530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612
    def PersonAdd(self,name,email,url,phone,passwd,roles):
        sr = name.rsplit(" ",1)
        if len(sr) == 1:
            fname = lname = name
        else:
            fname = sr[0]
            lname = sr[1]
            pass
        pid =  self.__server.AddPerson(self.auth,
                                       { 'first_name':fname,'last_name':lname,
                                         'url':url,'phone':phone,'email':email,
                                         'password':passwd,'enabled':True })
        for role in roles:
            self.__server.AddRoleToPerson(self.auth,role,pid)
            pass
        return pid

    def PersonUpdate(self,id,name,email,url,phone,passwd,roles,keys,sites):
        # first figure out if we need to update the person
        upd = {}
        if name:
            sr = name.rsplit(" ",1)
            if len(sr) == 1:
                upd['first_name'] = upd['last_name'] = name
            else:
                upd['first_name'] = sr[0]
                upd['last_name'] = sr[1]
                pass
            pass
        if email:
            upd['email'] = email
        if url:
            upd['url'] = url
        if phone:
            upd['phone'] = phone
        if passwd:
            upd['passwd'] = passwd

        # add doesn't enable, so we always force this.
        upd['enabled'] = True

        if len(upd.keys()) > 0:
            self.__server.UpdatePerson(self.auth,id,upd)
            pass

        # now the role:
        if roles:
            for role in roles:
                self.__server.AddRoleToPerson(self.auth,role,id)
                pass
            pass

        # now the keys:
        kil = self.KeyInfo(infilter={'person_id':id},outfilter=['key'])
        for k in keys:
            found = False
            for ki in kil:
                if ki['key'] == k:
                    found = True
                    break
                pass
            if not found:
                try:
                    self.__server.AddPersonKey(self.auth,id,{ 'key_type':'ssh',
                                                              'key':k })
                except xmlrpclib.Fault,ex:
                    if ex.faultString.find("Invalid SSH version 2 public key") \
                       > -1:
                        pass
                    else:
                        raise
                pass
            pass

        # and last the sites
        for site in sites:
            self.__server.AddPersonToSite(self.auth,id,site)
            pass

        return

    def PersonDelete(self,id):
        return self.__server.DeletePerson(self.auth,id)
613

614 615 616 617 618 619
    def PersonId(self,id):
        retval = self.PersonInfo(id,['person_id'])
        if retval and 'person_id' in retval:
            return retval['person_id']
        return None

620
    pass # end of PLCagent class
621 622 623

class mod_PLC4:
    
624
    def __init__(self,plc):
625
        self.modname = "mod_PLC4"
626
        self.plc = plc
627
        self.__PLCagent = None
628
        self.__defAgent = None
629 630 631
        self.__sliceexpdict = {}
        self.__sliceexptime = 0

632 633
        self.nmuid = plc.getAttrVal("nm_username",required=True)
        self.nmkey = plc.getAttrVal("nm_key",required=True)
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657

        # setup translation attributes
        self.sync_tr = dict()
        self.sync_tr['node'] = { 'add':'NodeAdd',
                                 'add_accepts':['site','hostname'],
                                 'upd':'NodeUpdate',
                                 'upd_accepts':['id','hostname','networks'],
                                 'del':'NodeDelete' }
        self.sync_tr['user'] = { 'add':'PersonAdd',
                                 'add_accepts':['name','email','url','phone',
                                                'passwd','roles'],
                                 'upd':'PersonUpdate',
                                 'upd_accepts':['id','name','email','url',
                                                'phone','passwd','roles',
                                                'keys','sites'],
                                 'del':'PersonDelete' }
        self.sync_tr['project'] = { 'add':'SiteAdd',
                                    'add_accepts':['name','url',
                                                   'longitude','latitude'],
                                    'upd':'SiteUpdate',
                                    'upd_accepts':['id','url',
                                                   'longitude','latitude'],
                                    'del':'SiteDelete' }

658 659 660
        self.mgmt_tr = dict()
        self.mgmt_tr['node'] = { 'setstate':'NodeSetBootState' }

661 662
        return

663 664 665 666 667 668
    def setdebug(self,sdebug,sverbose):
        global debug, verbose
        debug = sdebug
        verbose = sverbose
        pass

669 670 671 672 673 674 675 676 677 678 679 680 681 682 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
    def __filterdict(self,d,key_list):
        retval = dict()
        for k in key_list:
            if d.has_key(k):
                retval[k] = d[k]
                pass
            pass
        return retval

    def syncSupport(self):
        return self.sync_tr.keys()

    def syncObject(self,objtype,properties):
        agent = self.__getAgent(None)
        if not properties.has_key("id"):
            method = getattr(agent,self.sync_tr[objtype]['add'])
            args = self.sync_tr[objtype]['add_accepts']
            fargs = self.__filterdict(properties,args)
            if debug:
                print "adding object of type '%s' with args '%s'" \
                      % (objtype,fargs)
            retval = tryXmlrpcCmd(method,fargs)
            # NOTE: we also have to add the `id' field to properties so that
            # the subsequent update call works.
            properties['id'] = retval
        else:
            # cheat :-)
            retval = properties['id']
            pass

        method = getattr(agent,self.sync_tr[objtype]['upd'])
        args = self.sync_tr[objtype]['upd_accepts']
        fargs = self.__filterdict(properties,args)
        if debug:
            print "updating object of type '%s' with args '%s'" \
                  % (objtype,fargs)
        tryXmlrpcCmd(method,self.__filterdict(properties,args))

        return retval

709 710 711 712 713 714 715 716 717 718 719 720 721 722
    def manageObject(self,objtype,objid,op,opargs=[]):
        agent = self.__getAgent(None)

        method = getattr(agent,self.mgmt_tr[objtype][op])
        fargs = list(opargs)
        fargs.insert(0,objid)
        if debug:
            print "managing object of type '%s' with args '%s'" \
                  % (objtype,fargs)
        fargs = tuple(fargs)
        tryXmlrpcCmd(method,fargs)

        return objid

723 724 725 726 727 728
    def deleteObject(self,objtype,id):
        agent = self.__getAgent(None)

        method = getattr(agent,self.sync_tr[objtype]['del'])
        tryXmlrpcCmd(method,(id,))

729 730
        return

731
    def createSlice(self,slice):
732 733 734 735
        agent = self.__getAgent(slice.slicename)
        res = None
        now = calendar.timegm(time.gmtime())

736 737 738 739 740
        # fix up some slice info just in case
        if slice.instmethod == None:
            slice.instmethod = DEF_EMULAB_INSTMETHOD
            pass
        
741
        try:
742
            # XXX: fix to take desc and url args! (i.e., SliceUpdate)
743 744 745
            res = tryXmlrpcCmd(agent.SliceCreate,(slice.sliceurl,
                                                  slice.description,
                                                  slice.instmethod))
746 747 748 749 750
            if debug:
                print "SliceCreate result: %s" % res
                pass
            pass
        except:
751 752
            print "Failed to create slice %s at %s" % (slice.slicename,
                                                       self.plc.name)
753
            raise
754

755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780
        PLCticket = ''
        # We can only get the tickets if the slice is in delegated mode.
        if slice.instmethod == INSTMETHOD_DELEGATED:
            try:
                PLCticket = tryXmlrpcCmd(agent.SliceGetTicket)
                pass
            except:
                print "Failed to get PLC ticket for slice %s at %s" % \
                      (slice.slicename,self.plc.name)
                raise
            pass
        
        # XXX: fix for PLC 4
        leaseend = now + MAX_PLC_LEASELEN
        return (res, cPickle.dumps(PLCticket),leaseend)

    def configureSlice(self,slice):
        agent = self.__getAgent(slice.slicename)
        res = None
        now = calendar.timegm(time.gmtime())

        # fix up some slice info just in case
        if slice.instmethod == None:
            slice.instmethod = DEF_EMULAB_INSTMETHOD
            pass
        
781 782 783 784
        # If the slice is delegated, make sure to set the `delegations` attr
        # to our nm-controller slice
        if slice.instmethod == INSTMETHOD_DELEGATED:
            try:
785
                del_acct = self.plc.getAttrVal("nm_username",required=True)
786
                res = tryXmlrpcCmd(agent.SliceAddAttribute,
787 788
                                   ('delegations',del_acct),
                                   OKstrs = ['Slice attribute already exists'])
789 790 791 792 793 794 795 796 797
                if debug:
                    print "SliceAddAttribute(delegations) result: %s" % res
                    pass
                pass
            except:
                print "Failed to set 'delegations' attribute on slice %s" \
                      % slice.slicename
                raise
            pass
798 799

        # Allow slice attributes to be set "generically" via the db
800
        # Also see below where we add slice attributes on a per-node basis
801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821
        sas = self.plc.getAttrVal("slice_attributes",required=False)
        if sas:
            try:
                kvpairs = sas.split(',')
                for kvp in kvpairs:
                    (k,v) = kvp.split(':',1)

                    res = tryXmlrpcCmd(agent.SliceAddAttribute,(k,v),
                                       OKstrs = ['Slice attribute already exists'])
                    if debug:
                        print "SliceAddAttribute(%s,%s) result: %s" \
                              % (str(k),str(v),res)
                        pass
                    pass
                pass
            except:
                print "Failed to set slice attribute %s=%s on slice %s" \
                      % (str(k),str(v),slice.slicename)
                raise
            pass

822
        try:
823 824 825 826 827
            userlist = slice.getSliceUsers()
            # XXX: we might not always want to add emulabman
            if userlist == None:
                userlist = []
                pass
828 829 830 831 832 833 834 835 836 837
            elabman_acct = self.plc.getAttrVal("emulabman")
            if not elabman_acct == None:
                if not elabman_acct in userlist:
                    userlist.append(elabman_acct)
                    pass
                pass
            else:
                print "No emulabman account set for PLC %s or slice %s;\n" \
                      "  the portal will not work fully!" % (slice.plc.name,
                                                             slice.slicename)
838
                pass
839

840
            res = tryXmlrpcCmd(agent.SliceUsersAdd,
841
                               userlist)
842 843 844 845
            if debug:
                print "SliceUsersAdd result: %s" % res
                pass
        except:
846 847
            print "Failed to assign users %s to slice %s" % \
                  (str(userlist),slice.slicename)
848 849
            raise

850 851 852 853 854
        # The new NM will check to see if the slice has been added to the
        # nodes, and will delete the ticket if when it contacts PLC it finds
        # that the slice doesn't include the local NM's node... or something
        # like that... so we must add the nodes to the slice.
        try:
855
            tnodelist = slice.getSliceNodes()
856
            if tnodelist == None or tnodelist == []:
857
                pass
858 859 860 861 862 863
            else:
                nodelist = map(lambda x: x[2], tnodelist)
                res = tryXmlrpcCmd(agent.SliceNodesAdd, nodelist)
                if debug:
                    print "SliceNodesAdd result: %s" % res
                    pass
864 865 866 867 868 869
                pass
            pass
        except:
            print "Failed to add nodes to slice %s" % slice.slicename
            raise

870 871 872 873 874 875
        # add any per-node or per-nodegroup attributes:
        #
        # XXX: will not work as above -- need to fix above and add this
        # code in a way that works with the plab_attributes table.
        #

876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896
        # XXX: this is currently locked out to pi/user roles, although the docs
        # say otherwise, so we can't use it automatically yet.
        #
        # Add a proper_op, mount_dir, so that we can eventually replace
        # our current rootball load mechanism with proper.
        # NOTE: this is secure, even though it seems like we're not specifying
        # any access control.  ACLs consist of ".exportdir" and ".importdir"
        # files in the vserver directories to be imported into ("reverse
        # export") or exported.
        #try:
        #    res = tryXmlrpcCmd(agent.SliceAddAttribute,('proper_op',
        #                                                'mount_dir',))
        #    if debug:
        #        print "SliceAddAttribute result: %s" % str(res)
        #        pass
        #    pass
        #except:
        #    print "Failed to add proper_op:mount_dir to slice %s" \
        #          % slice.slicename
        #    raise

897
        PLCticket = ''
898 899 900 901
        # We can only get the tickets if the slice is in delegated mode.
        if slice.instmethod == INSTMETHOD_DELEGATED:
            try:
                PLCticket = tryXmlrpcCmd(agent.SliceGetTicket)
902
                pass
903 904 905
            except:
                print "Failed to get PLC ticket for slice %s" % slice.slicename
                raise
906
            pass
907
        
908 909
        # XXX: fix for PLC 4
        leaseend = now + MAX_PLC_LEASELEN
910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941
        return (res, cPickle.dumps(PLCticket),leaseend)

    def loadSliceInfo(self,slice):
        """
        Retrieves basic slice data from PLC and stores it in the slice obj.
        """
        agent = self.__getAgent(slice.slicename)
        outfilter = ['instantiation','created','url','max_nodes','expires',
                     'description','node_ids','person_ids']
        retval = tryXmlrpcCmd(agent.SliceInfo,([slice.slicename],outfilter))
        sdict = retval[0]

        # grab the basic stuff
        self.instmethod = sdict['instantiation']
        self.description = sdict['description']
        self.sliceurl = sdict['url']
        
        # now resolve the nodes...
        retval = tryXmlrpcCmd(agent.NodeInfo,(sdict['node_ids'],['hostname']))
        self.nodelist = []
        for d in retval:
            self.nodelist.append(d['hostname'])
            pass

        # now resolve the users...
        retval = tryXmlrpcCmd(agent.PersonInfo,(sdict['person_ids'],['email']))
        self.userlist = []
        for d in retval:
            self.userlist.append(d['email'])
            pass

        return
942
    
943 944 945 946 947 948 949
    def deleteSlice(self, slice):
        agent = self.__getAgent(slice.slicename)
        # XXX: fix OKstrs based on what plc actually returns
        tryXmlrpcCmd(agent.SliceDelete, OKstrs = ["does not exist"])
        pass

    def renewSlice(self, slice, force = False):
950 951 952 953
        """
        Returns -1 if did not need to renew; 0 if renew succeeded;
        1 if renew failed.
        """
954 955 956 957 958 959 960
        agent = self.__getAgent(slice.slicename)
        ret = 0
        now = int(time.time()) # seconds since the epoch (UTC)
 
        # Get current PLC timeout for this slice
        leaseend = self.getSliceExpTime(slice.slicename)

961
        if debug:
962 963
            print "%s says %s/%s expires at %s" % \
                  (slice.plc.name,slice.eid,slice.pid,
964 965 966
                   time.strftime("%Y-%m-%d %H:%M:%S",
                                 time.gmtime(leaseend)))

967 968 969
        # Warn that we weren't able to get the exp. time from PLC,
        # but don't fail - try to renew anyway.
        if not leaseend:
970 971
            print "Couldn't get slice expiration time from %s !" % \
                  slice.plc.name
972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996
            leaseend = slice.leaseend
            pass

        # Allow some slop in our recorded time versus PLC's.  This is necessary
        # since we calculate the expiration locally.  If we are off by too much
        # then adjust to PLC's recorded expiration.
        if abs(leaseend - slice.leaseend) > MAX_LEASE_SLOP:
            print "Warning: recorded lease for %s doesn't agree with PLC" % \
                  slice.slicename
            print "\tRecorded: %s  Actual: %s" % (slice.leaseend, leaseend)
            slice.leaseend = leaseend
            pass

        # Expired!  Just bitch about it; try renewal anyway.  The renewal
        # code in libplab will send email.
        if leaseend < now:
            print "Slice %s (%s/%s) has expired!" % \
                  (slice.slicename, slice.pid, slice.eid)
            pass

        # If the lease is at least as large as the minimum window,
        # don't bother renewing it.
        if leaseend - now > MIN_LEASE_WINDOW and not force:
            print "Slice %s (%s/%s) doesn't need to be renewed" % \
                  (slice.slicename, slice.pid, slice.eid)
997
            return -1
998 999

        # Max out leaseend as far as (politically) possible
1000 1001 1002 1003 1004 1005 1006
        newleaseend = now + MAX_PLC_LEASELEN

        if debug:
            print "Renewing until %s (adding now +%d s)" % \
                  (time.strftime("%Y-%m-%d %H:%M:%S",
                                 time.gmtime(newleaseend)),
                   newleaseend - now)
1007 1008 1009 1010 1011 1012 1013
        
        try:
            # XXX: again fix NOKstrs as necessary
            res = tryXmlrpcCmd(agent.SliceRenew,
                               newleaseend,
                               NOKstrs = ["does not exist"])
            # Get the updated ticket.
1014
            slice.slicemeta = self.getSliceMeta(slice)
1015
            slice.slicemeta_legacy = None
1016

1017
            ret = 0
1018 1019 1020 1021 1022 1023 1024
            if debug:
                print "SliceRenew returns: %s" % res
                pass
            pass
        except:
            print "Failed to renew lease for slice %s" % slice.slicename
            traceback.print_exc()
1025
            ret = 1
1026 1027 1028 1029 1030 1031 1032
            pass
        else:
            slice.leaseend = newleaseend
            pass
        
        return ret

1033 1034 1035 1036 1037 1038 1039 1040
    def getNodes(self,infilter=None,outfilter=None):
        """
        Basically, wraps the GetNodes method, but also lets the caller
        optimize the call by filtering.
        """
        agent = self.__getAgent(None)
        return tryXmlrpcCmd(agent.NodeInfo,(infilter,outfilter))

1041 1042 1043 1044 1045 1046 1047 1048
    def getNodeGroups(self,infilter=None,outfilter=None):
        """
        Basically, wraps the GetNodeGroups method, but also lets the caller
        optimize the call by filtering.
        """
        agent = self.__getAgent(None)
        return tryXmlrpcCmd(agent.NodeGroupInfo,(infilter,outfilter))

1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064
    def getNodeNetworks(self,infilter=None,outfilter=None):
        """
        Basically, wraps the GetNodeNetworks method, but also lets the caller
        optimize the call by filtering.
        """
        agent = self.__getAgent(None)
        return tryXmlrpcCmd(agent.NodeNetworkInfo,(infilter,outfilter))

    def getSites(self,infilter=None,outfilter=None):
        """
        Basically, wraps the GetSites method, but also lets the caller
        optimize the call by filtering.
        """
        agent = self.__getAgent(None)
        return tryXmlrpcCmd(agent.SiteInfo,(infilter,outfilter))

1065
    def getSliceMetaLegacy(self, slice):
1066
        agent = self.__getAgent(slice.slicename)
1067 1068 1069 1070

        # We can only get the tickets if the slice is in delegated mode.
        if slice.instmethod != INSTMETHOD_DELEGATED:
            return cPickle.dumps('')
1071 1072
        
        try:
1073
            PLCticket = tryXmlrpcCmd(agent.SliceGetTicketLegacy)
1074 1075
            pass
        except:
1076
            print "Failed to get legacy PLC ticket for slice %s" % slice.slicename
1077 1078 1079 1080
            raise

        return cPickle.dumps(PLCticket)

1081 1082
    def getSliceMeta(self,slice):
        agent = self.__getAgent(slice.slicename)
1083
        
1084 1085 1086 1087 1088 1089 1090 1091
        try:
            retval = tryXmlrpcCmd(agent.SliceGetTicket)
            pass
        except:
            print "Failed to get PLC ticket for slice %s" % slice.slicename
            raise
        return cPickle.dumps(retval)

1092 1093
    def pingNM(self,node):
        plcagent = self.__getAgent(node.slice.slicename)
1094
        node.nmagent = NM4agent(node.IP,node.nodeid,self.nmuid,self.nmkey)
1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105

        try:
            res = node.nmagent.ping()
            return res
        except:
            if debug:
                traceback.print_exc()
                pass
            pass
        return False

1106
    def createNode(self, node):
1107
        plcagent = self.__getAgent(node.slice.slicename)
1108

1109 1110 1111 1112 1113
        if debug:
            print "createNode for node %s (%s/%s) starting" \
                  % (str(node.nodeid),str(node.hostname),str(node.IP))
            pass

1114
        ticketdata = cPickle.loads(node.slice.slicemeta)
1115
        node.nmagent = NM4agent(node.IP,node.nodeid,self.nmuid,self.nmkey)
1116 1117 1118

        res = tryXmlrpcCmd(plcagent.SliceNodesAdd, node.hostname,
                           OKstrs = ["already assigned"])
1119 1120

        try:
1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134
            res = node.nmagent.deliver_ticket(ticketdata)
            if not res[0] == 0:
                raise RuntimeError, "deliver_ticket(%s/%s) failed: %d, %s" % \
                      (node.nodeid,node.IP,realres[0], realres[1])
            pass
        except:
            print "deliver_ticket(%s/%s), slice %s: exception\n%s" \
                  % (node.nodeid,node.IP,node.slice.slicename,
                     traceback.format_exc())
            # XXX: Can we clean up on the plab side here?
            #      delete_sliver requires an rcap, but we don't have one
            #      in this case (since sliver creation failed).
            # self.freeNode(node)
            raise
1135

1136 1137 1138
        try:
            res = node.nmagent.create_sliver(node.slice.slicename)
            if not res[0][0] == 0:
1139 1140
                raise RuntimeError, "create_sliver(%s/%s) failed: %d, %s" % \
                      (node.nodeid,node.IP,realres[0], realres[1])
1141 1142
            pass
        except:
1143 1144 1145
            print "create_sliver(%s/%s), slice %s: exception\n%s" \
                  % (node.nodeid,node.IP,node.slice.slicename,
                     traceback.format_exc())
1146 1147 1148 1149 1150 1151 1152
            # XXX: Can we clean up on the plab side here?
            #      delete_sliver requires an rcap, but we don't have one
            #      in this case (since sliver creation failed).
            # self.freeNode(node)
            raise

        # send back the rcap
1153 1154 1155
        #return (res, cPickle.dumps(res[1][0]), node.slice.leaseend)
        # instead, we send back whatever the wrapped agent sends back... which
        # is some form of rcap.
1156
        return (res[0],'',node.slice.leaseend)
1157
    
1158
    def freeNode(self, node):
1159 1160 1161 1162
        rcap = None
        try:
            rcap = cPickle.loads(node.nodemeta)
        except:
1163
            #print "WARNING: couldn't load rcap"
1164
            pass
1165
        node.nmagent = NM4agent(node.IP,node.nodeid,self.nmuid,self.nmkey)
1166 1167 1168
        res = None

        try:
1169
            res = node.nmagent.delete_sliver(node.slice.slicename)
1170 1171 1172 1173
            if not res[0] == 0:
                raise RuntimeError, "delete_sliver failed: %d" % res[0]
            pass
        except:
1174 1175
            print "Failed to release node %s/%s from slice %s" % \
                  (node.nodeid,node.IP,node.slice.slicename)
1176 1177 1178 1179 1180
            raise
        
        return res

    def renewNode(self, node, length = 0):
1181 1182
        return self.createNode(node)
    
1183
    def startNode(self,node):
1184
        node.nmagent = NM4agent(node.IP,node.nodeid,self.nmuid,self.nmkey)
1185 1186 1187 1188 1189 1190 1191
        try:
            res = node.nmagent.start_sliver(node.slice.slicename)
        except:
            print "Failed to start node %s/%s from slice %s" % \
                  (node.nodeid,node.IP,node.slice.slicename)
            raise
        return res
1192 1193

    def stopNode(self,node):
1194
        node.nmagent = NM4agent(node.IP,node.nodeid,self.nmuid,self.nmkey)
1195 1196 1197 1198 1199 1200 1201
        try:
            res = node.nmagent.stop_sliver(node.slice.slicename)
        except:
            print "Failed to stop node %s/%s from slice %s" % \
                  (node.nodeid,node.IP,node.slice.slicename)
            raise
        return res
1202

1203
    def restartNode(self,node):
1204
        node.nmagent = NM4agent(node.IP,node.nodeid,self.nmuid,self.nmkey)
1205 1206 1207
        try:
            res = node.nmagent.restart_sliver(node.slice.slicename)
        except:
1208
            print "Failed to restart node %s/%s from slice %s" % \
1209 1210 1211 1212
                  (node.nodeid,node.IP,node.slice.slicename)
            raise
        return res
    
1213
    # XXX: this is broken, appears to be bug in GetSlices xmlrpc call in PLC4
1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234
    def getSliceExpTime(self, slicename):
        """
        Grab the expiration time for a slice according to PLC.
        Entries are cached for a time specified in this module by
        MAX_CACHE_TIME.  Returns seconds since the epoch in UTC.
        """
        agent = self.__getAgent(slicename)
        # Refresh the slice expiration cache if:
        # 1) cache is cold
        # 2) cache is too old
        # 3) given slice is not in cache
        if not self.__sliceexpdict or \
           self.__sliceexptime < time.time() - MAX_CACHE_TIME or \
           not self.__sliceexpdict.has_key(slicename):
            #
            # This behavior (of getting ALL our slice expiration times)
            # can only be emulated by getting the slice list, and then
            # checking the expiration times for ALL slices in the list...
            # GetSlices in PLC 4 doesn't give you everything if you ask for
            # nothing.
            #
1235 1236 1237
            sdict = tryXmlrpcCmd(agent.SliceInfo,[slicename])
            # bug in PLC return filter...
            #                     ([slicename],['name','expires']))
1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251
            for entry in sdict:
                self.__sliceexpdict[entry['name']] = entry
                pass
            self.__sliceexptime = time.time()
            pass

        if not self.__sliceexpdict.has_key(slicename):
            print "Slice %s unknown to PLC" % slicename
            return None

        leaseend = self.__sliceexpdict[slicename]['expires']

        return leaseend

1252 1253 1254
    def doAuthCheck(self,slicename):
        return tryXmlrpcCmd(self.__getAgent(slicename).AuthCheck)

1255 1256 1257 1258
    def __getAgent(self, slicename):
        """
        Returns a PLC agent object for the specified slice.  May cache.
        """
1259 1260 1261 1262 1263 1264 1265
        # sometimes we're not asking about a particular slice
        if not slicename:
            if not self.__defAgent:
                self.__defAgent = PLCagent(self.plc,None)
                pass
            return self.__defAgent

1266 1267
        if not self.__PLCagent or \
           not self.__PLCagent.getSliceName() == slicename:
1268
            self.__PLCagent = PLCagent(self.plc,slicename)
1269 1270 1271 1272 1273
            pass

        return self.__PLCagent

    pass # end of mod_PLC class