ia64/xen-unstable

view tools/python/xen/xend/server/DevController.py @ 13012:903f80054bca

[XEND] Activate a declared external device migration script for all devices and all save modes.
Signed-off-by: Brendan Cully <brendan@cs.ubc.ca>
author kfraser@localhost.localdomain
date Thu Dec 14 10:00:56 2006 +0000 (2006-12-14)
parents dcca88fc849c
children ef5e6df3ba9e
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, XendRoot
23 from xen.xend.XendError import VmError
24 from xen.xend.XendLogging import log
26 from xen.xend.xenstore.xstransact import xstransact, complete
27 from xen.xend.xenstore.xswatch import xswatch
29 import os
31 DEVICE_CREATE_TIMEOUT = 100
32 HOTPLUG_STATUS_NODE = "hotplug-status"
33 HOTPLUG_ERROR_NODE = "hotplug-error"
34 HOTPLUG_STATUS_ERROR = "error"
35 HOTPLUG_STATUS_BUSY = "busy"
37 Connected = 1
38 Error = 2
39 Missing = 3
40 Timeout = 4
41 Busy = 5
43 xenbusState = {
44 'Unknown' : 0,
45 'Initialising' : 1,
46 'InitWait' : 2,
47 'Initialised' : 3,
48 'Connected' : 4,
49 'Closing' : 5,
50 'Closed' : 6,
51 }
53 xroot = XendRoot.instance()
55 xenbusState.update(dict(zip(xenbusState.values(), xenbusState.keys())))
58 class DevController:
59 """Abstract base class for a device controller. Device controllers create
60 appropriate entries in the store to trigger the creation, reconfiguration,
61 and destruction of devices in guest domains. Each subclass of
62 DevController is responsible for a particular device-class, and
63 understands the details of configuration specific to that device-class.
65 DevController itself provides the functionality common to all device
66 creation tasks, as well as providing an interface to XendDomainInfo for
67 triggering those events themselves.
68 """
70 # Set when registered.
71 deviceClass = None
74 ## public:
76 def __init__(self, vm):
77 self.vm = vm
80 def createDevice(self, config):
81 """Trigger the creation of a device with the given configuration.
83 @return The ID for the newly created device.
84 """
85 (devid, back, front) = self.getDeviceDetails(config)
86 if devid is None:
87 return 0
89 (backpath, frontpath) = self.addStoreEntries(config, devid, back,
90 front)
92 import xen.xend.XendDomain
93 xd = xen.xend.XendDomain.instance()
94 backdom_name = config.get('backend')
95 if backdom_name is None:
96 backdom = xen.xend.XendDomain.DOM0_ID
97 else:
98 bd = xd.domain_lookup_nr(backdom_name)
99 backdom = bd.getDomid()
100 count = 0
101 while True:
102 t = xstransact()
103 try:
104 if devid in self.deviceIDs(t):
105 if 'dev' in back:
106 dev_str = '%s (%d, %s)' % (back['dev'], devid,
107 self.deviceClass)
108 else:
109 dev_str = '%s (%s)' % (devid, self.deviceClass)
111 raise VmError("Device %s is already connected." % dev_str)
113 if count == 0:
114 log.debug('DevController: writing %s to %s.', str(front),
115 frontpath)
116 log.debug('DevController: writing %s to %s.', str(back),
117 backpath)
118 elif count % 50 == 0:
119 log.debug(
120 'DevController: still waiting to write device entries.')
122 t.remove(frontpath)
123 t.remove(backpath)
125 t.mkdir(backpath)
126 t.set_permissions(backpath,
127 {'dom': backdom },
128 {'dom' : self.vm.getDomid(),
129 'read' : True })
130 t.mkdir(frontpath)
131 t.set_permissions(frontpath,
132 {'dom': self.vm.getDomid()},
133 {'dom': backdom, 'read': True})
135 t.write2(frontpath, front)
136 t.write2(backpath, back)
138 if t.commit():
139 return devid
141 count += 1
142 except:
143 t.abort()
144 raise
147 def waitForDevices(self):
148 log.debug("Waiting for devices %s.", self.deviceClass)
149 return map(self.waitForDevice, self.deviceIDs())
152 def waitForDevice(self, devid):
153 log.debug("Waiting for %s.", devid)
155 status = self.waitForBackend(devid)
157 if status == Timeout:
158 self.destroyDevice(devid)
159 raise VmError("Device %s (%s) could not be connected. "
160 "Hotplug scripts not working." %
161 (devid, self.deviceClass))
163 elif status == Error:
164 self.destroyDevice(devid)
165 raise VmError("Device %s (%s) could not be connected. "
166 "Backend device not found." %
167 (devid, self.deviceClass))
169 elif status == Missing:
170 # Don't try to destroy the device; it's already gone away.
171 raise VmError("Device %s (%s) could not be connected. "
172 "Device not found." % (devid, self.deviceClass))
174 elif status == Busy:
175 err = None
176 frontpath = self.frontendPath(devid)
177 backpath = xstransact.Read(frontpath, "backend")
178 if backpath:
179 err = xstransact.Read(backpath, HOTPLUG_ERROR_NODE)
180 if not err:
181 err = "Busy."
183 self.destroyDevice(devid)
184 raise VmError("Device %s (%s) could not be connected.\n%s" %
185 (devid, self.deviceClass, err))
189 def reconfigureDevice(self, devid, config):
190 """Reconfigure the specified device.
192 The implementation here just raises VmError. This may be overridden
193 by those subclasses that can reconfigure their devices.
194 """
195 raise VmError('%s devices may not be reconfigured' % self.deviceClass)
198 def destroyDevice(self, devid):
199 """Destroy the specified device.
201 @param devid The device ID, or something device-specific from which
202 the device ID can be determined (such as a guest-side device name).
204 The implementation here simply deletes the appropriate paths from the
205 store. This may be overridden by subclasses who need to perform other
206 tasks on destruction. Further, the implementation here can only
207 accept integer device IDs, or values that can be converted to
208 integers. Subclasses may accept other values and convert them to
209 integers before passing them here.
210 """
212 devid = int(devid)
214 # Modify online status /before/ updating state (latter is watched by
215 # drivers, so this ordering avoids a race).
216 self.writeBackend(devid, 'online', "0")
217 self.writeBackend(devid, 'state', str(xenbusState['Closing']))
220 def configurations(self):
221 return map(self.configuration, self.deviceIDs())
224 def configuration(self, devid):
225 """@return an s-expression giving the current configuration of the
226 specified device. This would be suitable for giving to {@link
227 #createDevice} in order to recreate that device."""
228 configDict = self.getDeviceConfiguration(devid)
229 sxpr = [self.deviceClass]
230 for key, val in configDict.items():
231 if isinstance(val, (types.ListType, types.TupleType)):
232 for v in val:
233 if v != None:
234 sxpr.append([key, v])
235 else:
236 if val != None:
237 sxpr.append([key, val])
238 return sxpr
240 def sxprs(self):
241 """@return an s-expression describing all the devices of this
242 controller's device-class.
243 """
244 return xstransact.ListRecursive(self.frontendRoot())
247 def sxpr(self, devid):
248 """@return an s-expression describing the specified device.
249 """
250 return [self.deviceClass, ['dom', self.vm.getDomid(),
251 'id', devid]]
254 def getDeviceConfiguration(self, devid):
255 """Returns the configuration of a device.
257 @note: Similar to L{configuration} except it returns a dict.
258 @return: dict
259 """
260 backdomid = xstransact.Read(self.frontendPath(devid), "backend-id")
261 if backdomid is None:
262 raise VmError("Device %s not connected" % devid)
264 return {'backend': int(backdomid)}
266 def getAllDeviceConfigurations(self):
267 all_configs = {}
268 for devid in self.deviceIDs():
269 config_dict = self.getDeviceConfiguration(devid)
270 all_configs[devid] = config_dict
271 return all_configs
273 ## protected:
275 def getDeviceDetails(self, config):
276 """Compute the details for creation of a device corresponding to the
277 given configuration. These details consist of a tuple of (devID,
278 backDetails, frontDetails), where devID is the ID for the new device,
279 and backDetails and frontDetails are the device configuration
280 specifics for the backend and frontend respectively.
282 backDetails and frontDetails should be dictionaries, the keys and
283 values of which will be used as paths in the store. There is no need
284 for these dictionaries to include the references from frontend to
285 backend, nor vice versa, as these will be handled by DevController.
287 Abstract; must be implemented by every subclass.
289 @return (devID, backDetails, frontDetails), as specified above.
290 """
292 raise NotImplementedError()
294 def migrate(self, deviceConfig, network, dst, step, domName):
295 """ Migration of a device. The 'network' parameter indicates
296 whether the device is network-migrated (True). 'dst' then gives
297 the hostname of the machine to migrate to.
298 This function is called for 4 steps:
299 If step == 0: Check whether the device is ready to be migrated
300 or can at all be migrated; return a '-1' if
301 the device is NOT ready, a '0' otherwise. If it is
302 not ready ( = not possible to migrate this device),
303 migration will not take place.
304 step == 1: Called immediately after step 0; migration
305 of the kernel has started;
306 step == 2: Called after the suspend has been issued
307 to the domain and the domain is not scheduled anymore.
308 Synchronize with what was started in step 1, if necessary.
309 Now the device should initiate its transfer to the
310 given target. Since there might be more than just
311 one device initiating a migration, this step should
312 put the process performing the transfer into the
313 background and return immediately to achieve as much
314 concurrency as possible.
315 step == 3: Synchronize with the migration of the device that
316 was initiated in step 2.
317 Make sure that the migration has finished and only
318 then return from the call.
319 """
320 tool = xroot.get_external_migration_tool()
321 if tool:
322 log.info("Calling external migration tool for step %d" % step)
323 fd = os.popen("%s -type %s -step %d -host %s -domname %s" %
324 (tool, self.deviceClass, step, dst, domName))
325 for line in fd:
326 log.info(line.rstrip())
327 rc = fd.close()
328 if rc:
329 raise VmError('Migration tool returned %d' % (rc >> 8))
330 return 0
333 def recover_migrate(self, deviceConfig, network, dst, step, domName):
334 """ Recover from device migration. The given step was the
335 last one that was successfully executed.
336 """
337 tool = xroot.get_external_migration_tool()
338 if tool:
339 log.info("Calling external migration tool")
340 fd = os.popen("%s -type %s -step %d -host %s -domname %s -recover" %
341 (tool, self.deviceClass, step, dst, domName))
342 for line in fd:
343 log.info(line.rstrip())
344 rc = fd.close()
345 if rc:
346 raise VmError('Migration tool returned %d' % (rc >> 8))
347 return 0
350 def getDomid(self):
351 """Stub to {@link XendDomainInfo.getDomid}, for use by our
352 subclasses.
353 """
354 return self.vm.getDomid()
357 def allocateDeviceID(self):
358 """Allocate a device ID, allocating them consecutively on a
359 per-domain, per-device-class basis, and using the store to record the
360 next available ID.
362 This method is available to our subclasses, though it is not
363 compulsory to use it; subclasses may prefer to allocate IDs based upon
364 the device configuration instead.
365 """
366 path = self.frontendMiscPath()
367 return complete(path, self._allocateDeviceID)
370 def _allocateDeviceID(self, t):
371 result = t.read("nextDeviceID")
372 if result:
373 result = int(result)
374 else:
375 result = 0
376 t.write("nextDeviceID", str(result + 1))
377 return result
380 def readBackend(self, devid, *args):
381 frontpath = self.frontendPath(devid)
382 backpath = xstransact.Read(frontpath, "backend")
383 if backpath:
384 return xstransact.Read(backpath, *args)
385 else:
386 raise VmError("Device %s not connected" % devid)
388 def readFrontend(self, devid, *args):
389 return xstransact.Read(self.frontendPath(devid), *args)
391 def deviceIDs(self, transaction = None):
392 """@return The IDs of each of the devices currently configured for
393 this instance's deviceClass.
394 """
395 fe = self.backendRoot()
396 if transaction:
397 return map(lambda x: int(x.split('/')[-1]), transaction.list(fe))
398 else:
399 return map(int, xstransact.List(fe))
402 def writeBackend(self, devid, *args):
403 frontpath = self.frontendPath(devid)
404 backpath = xstransact.Read(frontpath, "backend")
406 if backpath:
407 xstransact.Write(backpath, *args)
408 else:
409 raise VmError("Device %s not connected" % devid)
412 ## private:
414 def addStoreEntries(self, config, devid, backDetails, frontDetails):
415 """Add to backDetails and frontDetails the entries to be written in
416 the store to trigger creation of a device. The backend domain ID is
417 taken from the given config, paths for frontend and backend are
418 computed, and these are added to the backDetails and frontDetails
419 dictionaries for writing to the store, including references from
420 frontend to backend and vice versa.
422 @return A pair of (backpath, frontpath). backDetails and frontDetails
423 will have been updated appropriately, also.
425 @param config The configuration of the device, as given to
426 {@link #createDevice}.
427 @param devid As returned by {@link #getDeviceDetails}.
428 @param backDetails As returned by {@link #getDeviceDetails}.
429 @param frontDetails As returned by {@link #getDeviceDetails}.
430 """
432 import xen.xend.XendDomain
433 xd = xen.xend.XendDomain.instance()
435 backdom_name = config.get('backend')
436 if backdom_name:
437 backdom = xd.domain_lookup_nr(backdom_name)
438 else:
439 backdom = xd.privilegedDomain()
441 if not backdom:
442 raise VmError("Cannot configure device for unknown backend %s" %
443 backdom_name)
445 frontpath = self.frontendPath(devid)
446 backpath = self.backendPath(backdom, devid)
448 frontDetails.update({
449 'backend' : backpath,
450 'backend-id' : "%i" % backdom.getDomid(),
451 'state' : str(xenbusState['Initialising'])
452 })
455 backDetails.update({
456 'domain' : self.vm.getName(),
457 'frontend' : frontpath,
458 'frontend-id' : "%i" % self.vm.getDomid(),
459 'state' : str(xenbusState['Initialising']),
460 'online' : "1"
461 })
463 return (backpath, frontpath)
466 def waitForBackend(self, devid):
468 frontpath = self.frontendPath(devid)
469 backpath = xstransact.Read(frontpath, "backend")
471 if backpath:
472 statusPath = backpath + '/' + HOTPLUG_STATUS_NODE
473 ev = Event()
474 result = { 'status': Timeout }
476 xswatch(statusPath, hotplugStatusCallback, ev, result)
478 ev.wait(DEVICE_CREATE_TIMEOUT)
479 return result['status']
480 else:
481 return Missing
484 def backendPath(self, backdom, devid):
485 """Construct backend path given the backend domain and device id.
487 @param backdom [XendDomainInfo] The backend domain info."""
489 return "%s/backend/%s/%s/%d" % (backdom.getDomainPath(),
490 self.deviceClass,
491 self.vm.getDomid(), devid)
494 def frontendPath(self, devid):
495 return "%s/%d" % (self.frontendRoot(), devid)
498 def frontendRoot(self):
499 return "%s/device/%s" % (self.vm.getDomainPath(), self.deviceClass)
501 def backendRoot(self):
502 """Construct backend root path assuming backend is domain 0."""
503 from xen.xend.XendDomain import DOM0_ID
504 from xen.xend.xenstore.xsutil import GetDomainPath
505 return "%s/backend/%s/%s" % (GetDomainPath(DOM0_ID),
506 self.deviceClass, self.vm.getDomid())
508 def frontendMiscPath(self):
509 return "%s/device-misc/%s" % (self.vm.getDomainPath(),
510 self.deviceClass)
513 def hotplugStatusCallback(statusPath, ev, result):
514 log.debug("hotplugStatusCallback %s.", statusPath)
516 status = xstransact.Read(statusPath)
518 if status is not None:
519 if status == HOTPLUG_STATUS_ERROR:
520 result['status'] = Error
521 elif status == HOTPLUG_STATUS_BUSY:
522 result['status'] = Busy
523 else:
524 result['status'] = Connected
525 else:
526 return 1
528 log.debug("hotplugStatusCallback %d.", result['status'])
530 ev.set()
531 return 0