ia64/xen-unstable

view tools/python/xen/xend/server/DevController.py @ 13871:ed9dba8e2c67

Improve hotplug script error reporting via xenstore.

Use this to detect and report up-front in vif-bridge script whether
the bridge device exists or not.

Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
author kfraser@localhost.localdomain
date Wed Feb 07 16:22:55 2007 +0000 (2007-02-07)
parents b111908dd70b
children 6524e02edbeb
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
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 xoptions = XendOptions.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
78 self.hotplug = True
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 if not self.hotplug:
156 return
158 (status, err) = self.waitForBackend(devid)
160 if status == Timeout:
161 self.destroyDevice(devid, False)
162 raise VmError("Device %s (%s) could not be connected. "
163 "Hotplug scripts not working." %
164 (devid, self.deviceClass))
166 elif status == Error:
167 self.destroyDevice(devid, False)
168 if err is None:
169 raise VmError("Device %s (%s) could not be connected. "
170 "Backend device not found." %
171 (devid, self.deviceClass))
172 else:
173 raise VmError("Device %s (%s) could not be connected. "
174 "%s" % (devid, self.deviceClass, err))
175 elif status == Missing:
176 # Don't try to destroy the device; it's already gone away.
177 raise VmError("Device %s (%s) could not be connected. "
178 "Device not found." % (devid, self.deviceClass))
180 elif status == Busy:
181 self.destroyDevice(devid, False)
182 if err is None:
183 err = "Busy."
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 = xoptions.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 = xoptions.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")
479 if backpath:
480 statusPath = backpath + '/' + HOTPLUG_STATUS_NODE
481 ev = Event()
482 result = { 'status': Timeout }
484 xswatch(statusPath, hotplugStatusCallback, ev, result)
486 ev.wait(DEVICE_CREATE_TIMEOUT)
488 err = xstransact.Read(backpath, HOTPLUG_ERROR_NODE)
490 return (result['status'], err)
491 else:
492 return (Missing, None)
495 def backendPath(self, backdom, devid):
496 """Construct backend path given the backend domain and device id.
498 @param backdom [XendDomainInfo] The backend domain info."""
500 return "%s/backend/%s/%s/%d" % (backdom.getDomainPath(),
501 self.deviceClass,
502 self.vm.getDomid(), devid)
505 def frontendPath(self, devid):
506 return "%s/%d" % (self.frontendRoot(), devid)
509 def frontendRoot(self):
510 return "%s/device/%s" % (self.vm.getDomainPath(), self.deviceClass)
512 def backendRoot(self):
513 """Construct backend root path assuming backend is domain 0."""
514 from xen.xend.XendDomain import DOM0_ID
515 from xen.xend.xenstore.xsutil import GetDomainPath
516 return "%s/backend/%s/%s" % (GetDomainPath(DOM0_ID),
517 self.deviceClass, self.vm.getDomid())
519 def frontendMiscPath(self):
520 return "%s/device-misc/%s" % (self.vm.getDomainPath(),
521 self.deviceClass)
524 def hotplugStatusCallback(statusPath, ev, result):
525 log.debug("hotplugStatusCallback %s.", statusPath)
527 status = xstransact.Read(statusPath)
529 if status is not None:
530 if status == HOTPLUG_STATUS_ERROR:
531 result['status'] = Error
532 elif status == HOTPLUG_STATUS_BUSY:
533 result['status'] = Busy
534 else:
535 result['status'] = Connected
536 else:
537 return 1
539 log.debug("hotplugStatusCallback %d.", result['status'])
541 ev.set()
542 return 0