ia64/xen-unstable

view tools/python/xen/xend/server/DevController.py @ 16988:df6b8bed2845

xend: Scrub vnc password for vfb from xend.log.
Signed-off-by: Masaki Kanno <kanno.masaki@jp.fujitsu.com>
author Keir Fraser <keir.fraser@citrix.com>
date Tue Feb 05 10:40:10 2008 +0000 (2008-02-05)
parents 09d8b6eb3131
children c6eeb71a85cf
line source
1 #============================================================================
2 # This library is free software; you can redistribute it and/or
3 # modify it under the terms of version 2.1 of the GNU Lesser General Public
4 # License as published by the Free Software Foundation.
5 #
6 # This library is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 # Lesser General Public License for more details.
10 #
11 # You should have received a copy of the GNU Lesser General Public
12 # License along with this library; if not, write to the Free Software
13 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
14 #============================================================================
15 # Copyright (C) 2004, 2005 Mike Wray <mike.wray@hp.com>
16 # Copyright (C) 2005 XenSource Ltd
17 #============================================================================
19 from threading import Event
20 import types
22 from xen.xend import sxp, XendOptions
23 from xen.xend.XendError import VmError
24 from xen.xend.XendLogging import log
25 import xen.xend.XendConfig
27 from xen.xend.xenstore.xstransact import xstransact, complete
28 from xen.xend.xenstore.xswatch import xswatch
30 import os
32 DEVICE_CREATE_TIMEOUT = 100
33 DEVICE_DESTROY_TIMEOUT = 100
34 HOTPLUG_STATUS_NODE = "hotplug-status"
35 HOTPLUG_ERROR_NODE = "hotplug-error"
36 HOTPLUG_STATUS_ERROR = "error"
37 HOTPLUG_STATUS_BUSY = "busy"
39 Connected = 1
40 Error = 2
41 Missing = 3
42 Timeout = 4
43 Busy = 5
44 Disconnected = 6
46 xenbusState = {
47 'Unknown' : 0,
48 'Initialising' : 1,
49 'InitWait' : 2,
50 'Initialised' : 3,
51 'Connected' : 4,
52 'Closing' : 5,
53 'Closed' : 6,
54 }
56 xoptions = XendOptions.instance()
58 xenbusState.update(dict(zip(xenbusState.values(), xenbusState.keys())))
61 class DevController:
62 """Abstract base class for a device controller. Device controllers create
63 appropriate entries in the store to trigger the creation, reconfiguration,
64 and destruction of devices in guest domains. Each subclass of
65 DevController is responsible for a particular device-class, and
66 understands the details of configuration specific to that device-class.
68 DevController itself provides the functionality common to all device
69 creation tasks, as well as providing an interface to XendDomainInfo for
70 triggering those events themselves.
71 """
73 # Set when registered.
74 deviceClass = None
77 ## public:
79 def __init__(self, vm):
80 self.vm = vm
81 self.hotplug = True
83 def createDevice(self, config):
84 """Trigger the creation of a device with the given configuration.
86 @return The ID for the newly created device.
87 """
88 (devid, back, front) = self.getDeviceDetails(config)
89 if devid is None:
90 return 0
92 (backpath, frontpath) = self.addStoreEntries(config, devid, back,
93 front)
95 import xen.xend.XendDomain
96 xd = xen.xend.XendDomain.instance()
97 backdom_name = config.get('backend')
98 if backdom_name is None:
99 backdom = xen.xend.XendDomain.DOM0_ID
100 else:
101 bd = xd.domain_lookup_nr(backdom_name)
102 backdom = bd.getDomid()
103 count = 0
104 while True:
105 t = xstransact()
106 try:
107 if devid in self.deviceIDs(t):
108 if 'dev' in back:
109 dev_str = '%s (%d, %s)' % (back['dev'], devid,
110 self.deviceClass)
111 else:
112 dev_str = '%s (%s)' % (devid, self.deviceClass)
114 raise VmError("Device %s is already connected." % dev_str)
116 if count == 0:
117 log.debug('DevController: writing %s to %s.',
118 str(front), frontpath)
119 log.debug('DevController: writing %s to %s.',
120 str(xen.xend.XendConfig.scrub_password(back)), backpath)
121 elif count % 50 == 0:
122 log.debug(
123 'DevController: still waiting to write device entries.')
125 t.remove(frontpath)
126 t.remove(backpath)
128 t.mkdir(backpath)
129 t.set_permissions(backpath,
130 {'dom': backdom },
131 {'dom' : self.vm.getDomid(),
132 'read' : True })
133 t.mkdir(frontpath)
134 t.set_permissions(frontpath,
135 {'dom': self.vm.getDomid()},
136 {'dom': backdom, 'read': True})
138 t.write2(frontpath, front)
139 t.write2(backpath, back)
141 if t.commit():
142 return devid
144 count += 1
145 except:
146 t.abort()
147 raise
150 def waitForDevices(self):
151 log.debug("Waiting for devices %s.", self.deviceClass)
152 return map(self.waitForDevice, self.deviceIDs())
155 def waitForDevice(self, devid):
156 log.debug("Waiting for %s.", devid)
158 if not self.hotplug:
159 return
161 (status, err) = self.waitForBackend(devid)
163 if status == Timeout:
164 self.destroyDevice(devid, False)
165 raise VmError("Device %s (%s) could not be connected. "
166 "Hotplug scripts not working." %
167 (devid, self.deviceClass))
169 elif status == Error:
170 self.destroyDevice(devid, False)
171 if err is None:
172 raise VmError("Device %s (%s) could not be connected. "
173 "Backend device not found." %
174 (devid, self.deviceClass))
175 else:
176 raise VmError("Device %s (%s) could not be connected. "
177 "%s" % (devid, self.deviceClass, err))
178 elif status == Missing:
179 # Don't try to destroy the device; it's already gone away.
180 raise VmError("Device %s (%s) could not be connected. "
181 "Device not found." % (devid, self.deviceClass))
183 elif status == Busy:
184 self.destroyDevice(devid, False)
185 if err is None:
186 err = "Busy."
187 raise VmError("Device %s (%s) could not be connected.\n%s" %
188 (devid, self.deviceClass, err))
191 def waitForDevice_destroy(self, devid, backpath):
192 log.debug("Waiting for %s - destroyDevice.", devid)
194 if not self.hotplug:
195 return
197 status = self.waitForBackend_destroy(backpath)
199 if status == Timeout:
200 raise VmError("Device %s (%s) could not be disconnected. " %
201 (devid, self.deviceClass))
204 def reconfigureDevice(self, devid, config):
205 """Reconfigure the specified device.
207 The implementation here just raises VmError. This may be overridden
208 by those subclasses that can reconfigure their devices.
209 """
210 raise VmError('%s devices may not be reconfigured' % self.deviceClass)
213 def destroyDevice(self, devid, force):
214 """Destroy the specified device.
216 @param devid The device ID, or something device-specific from which
217 the device ID can be determined (such as a guest-side device name).
219 The implementation here simply deletes the appropriate paths from the
220 store. This may be overridden by subclasses who need to perform other
221 tasks on destruction. The implementation here accepts integer device
222 IDs or paths containg integer deviceIDs, e.g. vfb/0. Subclasses may
223 accept other values and convert them to integers before passing them
224 here.
225 """
227 dev = self.convertToDeviceNumber(devid)
229 # Modify online status /before/ updating state (latter is watched by
230 # drivers, so this ordering avoids a race).
231 self.writeBackend(dev, 'online', "0")
232 self.writeBackend(dev, 'state', str(xenbusState['Closing']))
234 if force:
235 frontpath = self.frontendPath(dev)
236 backpath = xstransact.Read(frontpath, "backend")
237 if backpath:
238 xstransact.Remove(backpath)
239 xstransact.Remove(frontpath)
241 self.vm._removeVm("device/%s/%d" % (self.deviceClass, dev))
243 def configurations(self, transaction = None):
244 return map(lambda x: self.configuration(x, transaction), self.deviceIDs(transaction))
247 def configuration(self, devid, transaction = None):
248 """@return an s-expression giving the current configuration of the
249 specified device. This would be suitable for giving to {@link
250 #createDevice} in order to recreate that device."""
251 configDict = self.getDeviceConfiguration(devid, transaction)
252 sxpr = [self.deviceClass]
253 for key, val in configDict.items():
254 if isinstance(val, (types.ListType, types.TupleType)):
255 for v in val:
256 if v != None:
257 sxpr.append([key, v])
258 else:
259 if val != None:
260 sxpr.append([key, val])
261 return sxpr
263 def sxprs(self):
264 """@return an s-expression describing all the devices of this
265 controller's device-class.
266 """
267 return xstransact.ListRecursive(self.frontendRoot())
270 def sxpr(self, devid):
271 """@return an s-expression describing the specified device.
272 """
273 return [self.deviceClass, ['dom', self.vm.getDomid(),
274 'id', devid]]
277 def getDeviceConfiguration(self, devid, transaction = None):
278 """Returns the configuration of a device.
280 @note: Similar to L{configuration} except it returns a dict.
281 @return: dict
282 """
283 if transaction is None:
284 backdomid = xstransact.Read(self.frontendPath(devid), "backend-id")
285 else:
286 backdomid = transaction.read(self.frontendPath(devid) + "/backend-id")
287 if backdomid is None:
288 raise VmError("Device %s not connected" % devid)
290 return {'backend': int(backdomid)}
292 def getAllDeviceConfigurations(self):
293 all_configs = {}
294 for devid in self.deviceIDs():
295 config_dict = self.getDeviceConfiguration(devid)
296 all_configs[devid] = config_dict
297 return all_configs
300 def convertToDeviceNumber(self, devid):
301 try:
302 return int(devid)
303 except ValueError:
304 # Does devid contain devicetype/deviceid?
305 # Propogate exception if unable to find an integer devid
306 return int(type(devid) is str and devid.split('/')[-1] or None)
308 ## protected:
310 def getDeviceDetails(self, config):
311 """Compute the details for creation of a device corresponding to the
312 given configuration. These details consist of a tuple of (devID,
313 backDetails, frontDetails), where devID is the ID for the new device,
314 and backDetails and frontDetails are the device configuration
315 specifics for the backend and frontend respectively.
317 backDetails and frontDetails should be dictionaries, the keys and
318 values of which will be used as paths in the store. There is no need
319 for these dictionaries to include the references from frontend to
320 backend, nor vice versa, as these will be handled by DevController.
322 Abstract; must be implemented by every subclass.
324 @return (devID, backDetails, frontDetails), as specified above.
325 """
327 raise NotImplementedError()
329 def migrate(self, deviceConfig, network, dst, step, domName):
330 """ Migration of a device. The 'network' parameter indicates
331 whether the device is network-migrated (True). 'dst' then gives
332 the hostname of the machine to migrate to.
333 This function is called for 4 steps:
334 If step == 0: Check whether the device is ready to be migrated
335 or can at all be migrated; return a '-1' if
336 the device is NOT ready, a '0' otherwise. If it is
337 not ready ( = not possible to migrate this device),
338 migration will not take place.
339 step == 1: Called immediately after step 0; migration
340 of the kernel has started;
341 step == 2: Called after the suspend has been issued
342 to the domain and the domain is not scheduled anymore.
343 Synchronize with what was started in step 1, if necessary.
344 Now the device should initiate its transfer to the
345 given target. Since there might be more than just
346 one device initiating a migration, this step should
347 put the process performing the transfer into the
348 background and return immediately to achieve as much
349 concurrency as possible.
350 step == 3: Synchronize with the migration of the device that
351 was initiated in step 2.
352 Make sure that the migration has finished and only
353 then return from the call.
354 """
355 tool = xoptions.get_external_migration_tool()
356 if tool:
357 log.info("Calling external migration tool for step %d" % step)
358 fd = os.popen("%s -type %s -step %d -host %s -domname %s" %
359 (tool, self.deviceClass, step, dst, domName))
360 for line in fd:
361 log.info(line.rstrip())
362 rc = fd.close()
363 if rc:
364 raise VmError('Migration tool returned %d' % (rc >> 8))
365 return 0
368 def recover_migrate(self, deviceConfig, network, dst, step, domName):
369 """ Recover from device migration. The given step was the
370 last one that was successfully executed.
371 """
372 tool = xoptions.get_external_migration_tool()
373 if tool:
374 log.info("Calling external migration tool")
375 fd = os.popen("%s -type %s -step %d -host %s -domname %s -recover" %
376 (tool, self.deviceClass, step, dst, domName))
377 for line in fd:
378 log.info(line.rstrip())
379 rc = fd.close()
380 if rc:
381 raise VmError('Migration tool returned %d' % (rc >> 8))
382 return 0
385 def getDomid(self):
386 """Stub to {@link XendDomainInfo.getDomid}, for use by our
387 subclasses.
388 """
389 return self.vm.getDomid()
392 def allocateDeviceID(self):
393 """Allocate a device ID, allocating them consecutively on a
394 per-domain, per-device-class basis, and using the store to record the
395 next available ID.
397 This method is available to our subclasses, though it is not
398 compulsory to use it; subclasses may prefer to allocate IDs based upon
399 the device configuration instead.
400 """
401 path = self.frontendMiscPath()
402 return complete(path, self._allocateDeviceID)
405 def _allocateDeviceID(self, t):
406 result = t.read("nextDeviceID")
407 if result:
408 result = int(result)
409 else:
410 result = 0
411 t.write("nextDeviceID", str(result + 1))
412 return result
415 def readBackend(self, devid, *args):
416 frontpath = self.frontendPath(devid)
417 backpath = xstransact.Read(frontpath, "backend")
418 if backpath:
419 return xstransact.Read(backpath, *args)
420 else:
421 raise VmError("Device %s not connected" % devid)
423 def readBackendTxn(self, transaction, devid, *args):
424 frontpath = self.frontendPath(devid)
425 backpath = transaction.read(frontpath + "/backend")
426 if backpath:
427 paths = map(lambda x: backpath + "/" + x, args)
428 return transaction.read(*paths)
429 else:
430 raise VmError("Device %s not connected" % devid)
432 def readFrontend(self, devid, *args):
433 return xstransact.Read(self.frontendPath(devid), *args)
435 def readFrontendTxn(self, transaction, devid, *args):
436 paths = map(lambda x: self.frontendPath(devid) + "/" + x, args)
437 return transaction.read(*paths)
439 def deviceIDs(self, transaction = None):
440 """@return The IDs of each of the devices currently configured for
441 this instance's deviceClass.
442 """
443 fe = self.backendRoot()
445 if transaction:
446 return map(lambda x: int(x.split('/')[-1]), transaction.list(fe))
447 else:
448 return map(int, xstransact.List(fe))
451 def writeBackend(self, devid, *args):
452 frontpath = self.frontendPath(devid)
453 backpath = xstransact.Read(frontpath, "backend")
455 if backpath:
456 xstransact.Write(backpath, *args)
457 else:
458 raise VmError("Device %s not connected" % devid)
461 ## private:
463 def addStoreEntries(self, config, devid, backDetails, frontDetails):
464 """Add to backDetails and frontDetails the entries to be written in
465 the store to trigger creation of a device. The backend domain ID is
466 taken from the given config, paths for frontend and backend are
467 computed, and these are added to the backDetails and frontDetails
468 dictionaries for writing to the store, including references from
469 frontend to backend and vice versa.
471 @return A pair of (backpath, frontpath). backDetails and frontDetails
472 will have been updated appropriately, also.
474 @param config The configuration of the device, as given to
475 {@link #createDevice}.
476 @param devid As returned by {@link #getDeviceDetails}.
477 @param backDetails As returned by {@link #getDeviceDetails}.
478 @param frontDetails As returned by {@link #getDeviceDetails}.
479 """
481 import xen.xend.XendDomain
482 xd = xen.xend.XendDomain.instance()
484 backdom_name = config.get('backend')
485 if backdom_name:
486 backdom = xd.domain_lookup_nr(backdom_name)
487 else:
488 backdom = xd.privilegedDomain()
490 if not backdom:
491 raise VmError("Cannot configure device for unknown backend %s" %
492 backdom_name)
494 frontpath = self.frontendPath(devid)
495 backpath = self.backendPath(backdom, devid)
497 frontDetails.update({
498 'backend' : backpath,
499 'backend-id' : "%i" % backdom.getDomid(),
500 'state' : str(xenbusState['Initialising'])
501 })
503 if self.vm.native_protocol:
504 frontDetails.update({'protocol' : self.vm.native_protocol})
506 backDetails.update({
507 'domain' : self.vm.getName(),
508 'frontend' : frontpath,
509 'frontend-id' : "%i" % self.vm.getDomid(),
510 'state' : str(xenbusState['Initialising']),
511 'online' : "1"
512 })
514 return (backpath, frontpath)
517 def waitForBackend(self, devid):
519 frontpath = self.frontendPath(devid)
520 # lookup a phantom
521 phantomPath = xstransact.Read(frontpath, 'phantom_vbd')
522 if phantomPath is not None:
523 log.debug("Waiting for %s's phantom %s.", devid, phantomPath)
524 statusPath = phantomPath + '/' + HOTPLUG_STATUS_NODE
525 ev = Event()
526 result = { 'status': Timeout }
527 xswatch(statusPath, hotplugStatusCallback, ev, result)
528 ev.wait(DEVICE_CREATE_TIMEOUT)
529 err = xstransact.Read(statusPath, HOTPLUG_ERROR_NODE)
530 if result['status'] != 'Connected':
531 return (result['status'], err)
533 backpath = xstransact.Read(frontpath, "backend")
536 if backpath:
537 statusPath = backpath + '/' + HOTPLUG_STATUS_NODE
538 ev = Event()
539 result = { 'status': Timeout }
541 xswatch(statusPath, hotplugStatusCallback, ev, result)
543 ev.wait(DEVICE_CREATE_TIMEOUT)
545 err = xstransact.Read(backpath, HOTPLUG_ERROR_NODE)
547 return (result['status'], err)
548 else:
549 return (Missing, None)
552 def waitForBackend_destroy(self, backpath):
554 statusPath = backpath + '/' + HOTPLUG_STATUS_NODE
555 ev = Event()
556 result = { 'status': Timeout }
558 xswatch(statusPath, deviceDestroyCallback, ev, result)
560 ev.wait(DEVICE_DESTROY_TIMEOUT)
562 return result['status']
565 def backendPath(self, backdom, devid):
566 """Construct backend path given the backend domain and device id.
568 @param backdom [XendDomainInfo] The backend domain info."""
570 return "%s/backend/%s/%s/%d" % (backdom.getDomainPath(),
571 self.deviceClass,
572 self.vm.getDomid(), devid)
575 def frontendPath(self, devid):
576 return "%s/%d" % (self.frontendRoot(), devid)
579 def frontendRoot(self):
580 return "%s/device/%s" % (self.vm.getDomainPath(), self.deviceClass)
582 def backendRoot(self):
583 """Construct backend root path assuming backend is domain 0."""
584 from xen.xend.XendDomain import DOM0_ID
585 from xen.xend.xenstore.xsutil import GetDomainPath
586 return "%s/backend/%s/%s" % (GetDomainPath(DOM0_ID),
587 self.deviceClass, self.vm.getDomid())
589 def frontendMiscPath(self):
590 return "%s/device-misc/%s" % (self.vm.getDomainPath(),
591 self.deviceClass)
594 def hotplugStatusCallback(statusPath, ev, result):
595 log.debug("hotplugStatusCallback %s.", statusPath)
597 status = xstransact.Read(statusPath)
599 if status is not None:
600 if status == HOTPLUG_STATUS_ERROR:
601 result['status'] = Error
602 elif status == HOTPLUG_STATUS_BUSY:
603 result['status'] = Busy
604 else:
605 result['status'] = Connected
606 else:
607 return 1
609 log.debug("hotplugStatusCallback %d.", result['status'])
611 ev.set()
612 return 0
615 def deviceDestroyCallback(statusPath, ev, result):
616 log.debug("deviceDestroyCallback %s.", statusPath)
618 status = xstransact.Read(statusPath)
620 if status is None:
621 result['status'] = Disconnected
622 else:
623 return 1
625 log.debug("deviceDestroyCallback %d.", result['status'])
627 ev.set()
628 return 0