ia64/xen-unstable

view tools/python/xen/xend/server/DevController.py @ 13014:ef5e6df3ba9e

[Xend] Add a --force option to detach operations.

In some situations triggered by errors found on guest side,
the device will be held forever, due to the online flag being still
on. Detach operations mostly fails from this point on, as the backend
will not see frontend's state change anymore.

Signed-off-by: Glauber de Oliveira Costa <gcosta@redhat.com>
author kfraser@localhost.localdomain
date Thu Dec 14 10:17:37 2006 +0000 (2006-12-14)
parents 903f80054bca
children 85c1e2383e98
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, force):
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']))
219 if force:
220 frontpath = self.frontendPath(devid)
221 backpath = xstransact.Read(frontpath, "backend")
222 if backpath:
223 xstransact.Remove(backpath)
224 xstransact.Remove(frontpath)
227 def configurations(self):
228 return map(self.configuration, self.deviceIDs())
231 def configuration(self, devid):
232 """@return an s-expression giving the current configuration of the
233 specified device. This would be suitable for giving to {@link
234 #createDevice} in order to recreate that device."""
235 configDict = self.getDeviceConfiguration(devid)
236 sxpr = [self.deviceClass]
237 for key, val in configDict.items():
238 if isinstance(val, (types.ListType, types.TupleType)):
239 for v in val:
240 if v != None:
241 sxpr.append([key, v])
242 else:
243 if val != None:
244 sxpr.append([key, val])
245 return sxpr
247 def sxprs(self):
248 """@return an s-expression describing all the devices of this
249 controller's device-class.
250 """
251 return xstransact.ListRecursive(self.frontendRoot())
254 def sxpr(self, devid):
255 """@return an s-expression describing the specified device.
256 """
257 return [self.deviceClass, ['dom', self.vm.getDomid(),
258 'id', devid]]
261 def getDeviceConfiguration(self, devid):
262 """Returns the configuration of a device.
264 @note: Similar to L{configuration} except it returns a dict.
265 @return: dict
266 """
267 backdomid = xstransact.Read(self.frontendPath(devid), "backend-id")
268 if backdomid is None:
269 raise VmError("Device %s not connected" % devid)
271 return {'backend': int(backdomid)}
273 def getAllDeviceConfigurations(self):
274 all_configs = {}
275 for devid in self.deviceIDs():
276 config_dict = self.getDeviceConfiguration(devid)
277 all_configs[devid] = config_dict
278 return all_configs
280 ## protected:
282 def getDeviceDetails(self, config):
283 """Compute the details for creation of a device corresponding to the
284 given configuration. These details consist of a tuple of (devID,
285 backDetails, frontDetails), where devID is the ID for the new device,
286 and backDetails and frontDetails are the device configuration
287 specifics for the backend and frontend respectively.
289 backDetails and frontDetails should be dictionaries, the keys and
290 values of which will be used as paths in the store. There is no need
291 for these dictionaries to include the references from frontend to
292 backend, nor vice versa, as these will be handled by DevController.
294 Abstract; must be implemented by every subclass.
296 @return (devID, backDetails, frontDetails), as specified above.
297 """
299 raise NotImplementedError()
301 def migrate(self, deviceConfig, network, dst, step, domName):
302 """ Migration of a device. The 'network' parameter indicates
303 whether the device is network-migrated (True). 'dst' then gives
304 the hostname of the machine to migrate to.
305 This function is called for 4 steps:
306 If step == 0: Check whether the device is ready to be migrated
307 or can at all be migrated; return a '-1' if
308 the device is NOT ready, a '0' otherwise. If it is
309 not ready ( = not possible to migrate this device),
310 migration will not take place.
311 step == 1: Called immediately after step 0; migration
312 of the kernel has started;
313 step == 2: Called after the suspend has been issued
314 to the domain and the domain is not scheduled anymore.
315 Synchronize with what was started in step 1, if necessary.
316 Now the device should initiate its transfer to the
317 given target. Since there might be more than just
318 one device initiating a migration, this step should
319 put the process performing the transfer into the
320 background and return immediately to achieve as much
321 concurrency as possible.
322 step == 3: Synchronize with the migration of the device that
323 was initiated in step 2.
324 Make sure that the migration has finished and only
325 then return from the call.
326 """
327 tool = xroot.get_external_migration_tool()
328 if tool:
329 log.info("Calling external migration tool for step %d" % step)
330 fd = os.popen("%s -type %s -step %d -host %s -domname %s" %
331 (tool, self.deviceClass, step, dst, domName))
332 for line in fd:
333 log.info(line.rstrip())
334 rc = fd.close()
335 if rc:
336 raise VmError('Migration tool returned %d' % (rc >> 8))
337 return 0
340 def recover_migrate(self, deviceConfig, network, dst, step, domName):
341 """ Recover from device migration. The given step was the
342 last one that was successfully executed.
343 """
344 tool = xroot.get_external_migration_tool()
345 if tool:
346 log.info("Calling external migration tool")
347 fd = os.popen("%s -type %s -step %d -host %s -domname %s -recover" %
348 (tool, self.deviceClass, step, dst, domName))
349 for line in fd:
350 log.info(line.rstrip())
351 rc = fd.close()
352 if rc:
353 raise VmError('Migration tool returned %d' % (rc >> 8))
354 return 0
357 def getDomid(self):
358 """Stub to {@link XendDomainInfo.getDomid}, for use by our
359 subclasses.
360 """
361 return self.vm.getDomid()
364 def allocateDeviceID(self):
365 """Allocate a device ID, allocating them consecutively on a
366 per-domain, per-device-class basis, and using the store to record the
367 next available ID.
369 This method is available to our subclasses, though it is not
370 compulsory to use it; subclasses may prefer to allocate IDs based upon
371 the device configuration instead.
372 """
373 path = self.frontendMiscPath()
374 return complete(path, self._allocateDeviceID)
377 def _allocateDeviceID(self, t):
378 result = t.read("nextDeviceID")
379 if result:
380 result = int(result)
381 else:
382 result = 0
383 t.write("nextDeviceID", str(result + 1))
384 return result
387 def readBackend(self, devid, *args):
388 frontpath = self.frontendPath(devid)
389 backpath = xstransact.Read(frontpath, "backend")
390 if backpath:
391 return xstransact.Read(backpath, *args)
392 else:
393 raise VmError("Device %s not connected" % devid)
395 def readFrontend(self, devid, *args):
396 return xstransact.Read(self.frontendPath(devid), *args)
398 def deviceIDs(self, transaction = None):
399 """@return The IDs of each of the devices currently configured for
400 this instance's deviceClass.
401 """
402 fe = self.backendRoot()
403 if transaction:
404 return map(lambda x: int(x.split('/')[-1]), transaction.list(fe))
405 else:
406 return map(int, xstransact.List(fe))
409 def writeBackend(self, devid, *args):
410 frontpath = self.frontendPath(devid)
411 backpath = xstransact.Read(frontpath, "backend")
413 if backpath:
414 xstransact.Write(backpath, *args)
415 else:
416 raise VmError("Device %s not connected" % devid)
419 ## private:
421 def addStoreEntries(self, config, devid, backDetails, frontDetails):
422 """Add to backDetails and frontDetails the entries to be written in
423 the store to trigger creation of a device. The backend domain ID is
424 taken from the given config, paths for frontend and backend are
425 computed, and these are added to the backDetails and frontDetails
426 dictionaries for writing to the store, including references from
427 frontend to backend and vice versa.
429 @return A pair of (backpath, frontpath). backDetails and frontDetails
430 will have been updated appropriately, also.
432 @param config The configuration of the device, as given to
433 {@link #createDevice}.
434 @param devid As returned by {@link #getDeviceDetails}.
435 @param backDetails As returned by {@link #getDeviceDetails}.
436 @param frontDetails As returned by {@link #getDeviceDetails}.
437 """
439 import xen.xend.XendDomain
440 xd = xen.xend.XendDomain.instance()
442 backdom_name = config.get('backend')
443 if backdom_name:
444 backdom = xd.domain_lookup_nr(backdom_name)
445 else:
446 backdom = xd.privilegedDomain()
448 if not backdom:
449 raise VmError("Cannot configure device for unknown backend %s" %
450 backdom_name)
452 frontpath = self.frontendPath(devid)
453 backpath = self.backendPath(backdom, devid)
455 frontDetails.update({
456 'backend' : backpath,
457 'backend-id' : "%i" % backdom.getDomid(),
458 'state' : str(xenbusState['Initialising'])
459 })
462 backDetails.update({
463 'domain' : self.vm.getName(),
464 'frontend' : frontpath,
465 'frontend-id' : "%i" % self.vm.getDomid(),
466 'state' : str(xenbusState['Initialising']),
467 'online' : "1"
468 })
470 return (backpath, frontpath)
473 def waitForBackend(self, devid):
475 frontpath = self.frontendPath(devid)
476 backpath = xstransact.Read(frontpath, "backend")
478 if backpath:
479 statusPath = backpath + '/' + HOTPLUG_STATUS_NODE
480 ev = Event()
481 result = { 'status': Timeout }
483 xswatch(statusPath, hotplugStatusCallback, ev, result)
485 ev.wait(DEVICE_CREATE_TIMEOUT)
486 return result['status']
487 else:
488 return Missing
491 def backendPath(self, backdom, devid):
492 """Construct backend path given the backend domain and device id.
494 @param backdom [XendDomainInfo] The backend domain info."""
496 return "%s/backend/%s/%s/%d" % (backdom.getDomainPath(),
497 self.deviceClass,
498 self.vm.getDomid(), devid)
501 def frontendPath(self, devid):
502 return "%s/%d" % (self.frontendRoot(), devid)
505 def frontendRoot(self):
506 return "%s/device/%s" % (self.vm.getDomainPath(), self.deviceClass)
508 def backendRoot(self):
509 """Construct backend root path assuming backend is domain 0."""
510 from xen.xend.XendDomain import DOM0_ID
511 from xen.xend.xenstore.xsutil import GetDomainPath
512 return "%s/backend/%s/%s" % (GetDomainPath(DOM0_ID),
513 self.deviceClass, self.vm.getDomid())
515 def frontendMiscPath(self):
516 return "%s/device-misc/%s" % (self.vm.getDomainPath(),
517 self.deviceClass)
520 def hotplugStatusCallback(statusPath, ev, result):
521 log.debug("hotplugStatusCallback %s.", statusPath)
523 status = xstransact.Read(statusPath)
525 if status is not None:
526 if status == HOTPLUG_STATUS_ERROR:
527 result['status'] = Error
528 elif status == HOTPLUG_STATUS_BUSY:
529 result['status'] = Busy
530 else:
531 result['status'] = Connected
532 else:
533 return 1
535 log.debug("hotplugStatusCallback %d.", result['status'])
537 ev.set()
538 return 0