Package buildbot :: Module libvirtbuildslave
[frames] | no frames]

Source Code for Module buildbot.libvirtbuildslave

  1  # This file is part of Buildbot.  Buildbot is free software: you can 
  2  # redistribute it and/or modify it under the terms of the GNU General Public 
  3  # License as published by the Free Software Foundation, version 2. 
  4  # 
  5  # This program is distributed in the hope that it will be useful, but WITHOUT 
  6  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
  8  # details. 
  9  # 
 10  # You should have received a copy of the GNU General Public License along with 
 11  # this program; if not, write to the Free Software Foundation, Inc., 51 
 12  # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 13  # 
 14  # Portions Copyright Buildbot Team Members 
 15  # Portions Copyright 2010 Isotoma Limited 
 16   
 17  import os 
 18   
 19  from twisted.internet import defer, utils, reactor, threads 
 20  from twisted.python import log 
 21  from buildbot.buildslave import AbstractBuildSlave, AbstractLatentBuildSlave 
 22   
 23  import libvirt 
 24   
 25   
26 -class WorkQueue(object):
27 """ 28 I am a class that turns parallel access into serial access. 29 30 I exist because we want to run libvirt access in threads as we don't 31 trust calls not to block, but under load libvirt doesnt seem to like 32 this kind of threaded use. 33 """ 34
35 - def __init__(self):
36 self.queue = []
37
38 - def _process(self):
39 log.msg("Looking to start a piece of work now...") 40 41 # Is there anything to do? 42 if not self.queue: 43 log.msg("_process called when there is no work") 44 return 45 46 # Peek at the top of the stack - get a function to call and 47 # a deferred to fire when its all over 48 d, next_operation, args, kwargs = self.queue[0] 49 50 # Start doing some work - expects a deferred 51 try: 52 d2 = next_operation(*args, **kwargs) 53 except: 54 d2 = defer.fail() 55 56 # Whenever a piece of work is done, whether it worked or not 57 # call this to schedule the next piece of work 58 def _work_done(res): 59 log.msg("Completed a piece of work") 60 self.queue.pop(0) 61 if self.queue: 62 log.msg("Preparing next piece of work") 63 reactor.callLater(0, self._process) 64 return res
65 d2.addBoth(_work_done) 66 67 # When the work is done, trigger d 68 d2.chainDeferred(d)
69
70 - def execute(self, cb, *args, **kwargs):
71 kickstart_processing = not self.queue 72 d = defer.Deferred() 73 self.queue.append((d, cb, args, kwargs)) 74 if kickstart_processing: 75 self._process() 76 return d
77
78 - def executeInThread(self, cb, *args, **kwargs):
79 return self.execute(threads.deferToThread, cb, *args, **kwargs)
80 81 82 # A module is effectively a singleton class, so this is OK 83 queue = WorkQueue() 84 85
86 -class Domain(object):
87 88 """ 89 I am a wrapper around a libvirt Domain object 90 """ 91
92 - def __init__(self, connection, domain):
93 self.connection = connection 94 self.domain = domain
95
96 - def create(self):
97 return queue.executeInThread(self.domain.create)
98
99 - def shutdown(self):
100 return queue.executeInThread(self.domain.shutdown)
101
102 - def destroy(self):
103 return queue.executeInThread(self.domain.destroy)
104 105
106 -class Connection(object):
107 108 """ 109 I am a wrapper around a libvirt Connection object. 110 """ 111
112 - def __init__(self, uri):
113 self.uri = uri 114 self.connection = libvirt.open(uri)
115
116 - def lookupByName(self, name):
117 """ I lookup an existing prefined domain """ 118 d = queue.executeInThread(self.connection.lookupByName, name) 119 def _(res): 120 return Domain(self, res)
121 d.addCallback(_) 122 return d
123
124 - def create(self, xml):
125 """ I take libvirt XML and start a new VM """ 126 d = queue.executeInThread(self.connection.createXML, xml, 0) 127 def _(res): 128 return Domain(self, res)
129 d.addCallback(_) 130 return d 131 132
133 -class LibVirtSlave(AbstractLatentBuildSlave):
134
135 - def __init__(self, name, password, connection, hd_image, base_image = None, xml=None, max_builds=None, notify_on_missing=[], 136 missing_timeout=60*20, build_wait_timeout=60*10, properties={}, locks=None):
137 AbstractLatentBuildSlave.__init__(self, name, password, max_builds, notify_on_missing, 138 missing_timeout, build_wait_timeout, properties, locks) 139 self.name = name 140 self.connection = connection 141 self.image = hd_image 142 self.base_image = base_image 143 self.xml = xml 144 145 self.insubstantiate_after_build = True 146 self.cheap_copy = True 147 self.graceful_shutdown = False 148 149 self.domain = None
150
151 - def _prepare_base_image(self):
152 """ 153 I am a private method for creating (possibly cheap) copies of a 154 base_image for start_instance to boot. 155 """ 156 if not self.base_image: 157 return defer.succeed(True) 158 159 if self.cheap_copy: 160 clone_cmd = "qemu-img" 161 clone_args = "create -b %(base)s -f qcow2 %(image)s" 162 else: 163 clone_cmd = "cp" 164 clone_args = "%(base)s %(image)s" 165 166 clone_args = clone_args % { 167 "base": self.base_image, 168 "image": self.image, 169 } 170 171 log.msg("Cloning base image: %s %s'" % (clone_cmd, clone_args)) 172 173 def _log_result(res): 174 log.msg("Cloning exit code was: %d" % res) 175 return res
176 177 d = utils.getProcessValue(clone_cmd, clone_args.split()) 178 d.addBoth(_log_result) 179 return d
180
181 - def start_instance(self, build):
182 """ 183 I start a new instance of a VM. 184 185 If a base_image is specified, I will make a clone of that otherwise i will 186 use image directly. 187 188 If i'm not given libvirt domain definition XML, I will look for my name 189 in the list of defined virtual machines and start that. 190 """ 191 if self.domain is not None: 192 raise ValueError('domain active') 193 194 d = self._prepare_base_image() 195 196 def _start(res): 197 if self.xml: 198 d = self.connection.create(self.xml) 199 def _xml_start(res): 200 self.domain = res 201 return
202 d.addCallback(_xml_start) 203 return d 204 d = self.connection.lookupByName(self.name) 205 def _really_start(res): 206 self.domain = res 207 return self.domain.create() 208 d.addCallback(_really_start) 209 return d 210 d.addCallback(_start) 211 212 def _started(res): 213 return True 214 d.addCallback(_started) 215 216 def _start_failed(failure): 217 log.msg("Cannot start a VM (%s), failing gracefully and triggering a new build check" % self.name) 218 log.err(failure) 219 self.domain = None 220 return False 221 d.addErrback(_start_failed) 222 223 return d 224
225 - def stop_instance(self, fast=False):
226 """ 227 I attempt to stop a running VM. 228 I make sure any connection to the slave is removed. 229 If the VM was using a cloned image, I remove the clone 230 When everything is tidied up, I ask that bbot looks for work to do 231 """ 232 log.msg("Attempting to stop '%s'" % self.name) 233 if self.domain is None: 234 log.msg("I don't think that domain is evening running, aborting") 235 return defer.succeed(None) 236 237 domain = self.domain 238 self.domain = None 239 240 if self.graceful_shutdown and not fast: 241 log.msg("Graceful shutdown chosen for %s" % self.name) 242 d = domain.shutdown() 243 else: 244 d = domain.destroy() 245 246 def _disconnect(res): 247 log.msg("VM destroyed (%s): Forcing its connection closed." % self.name) 248 return AbstractBuildSlave.disconnect(self)
249 d.addCallback(_disconnect) 250 251 def _disconnected(res): 252 log.msg("We forced disconnection (%s), cleaning up and triggering new build" % self.name) 253 if self.base_image: 254 os.remove(self.image) 255 self.botmaster.maybeStartBuildsForSlave(self.name) 256 return res 257 d.addBoth(_disconnected) 258 259 return d 260
261 - def buildFinished(self, *args, **kwargs):
262 """ 263 I insubstantiate a slave after it has done a build, if that is 264 desired behaviour. 265 """ 266 AbstractLatentBuildSlave.buildFinished(self, *args, **kwargs) 267 if self.insubstantiate_after_build: 268 log.msg("Got buildFinished notification - attempting to insubstantiate") 269 self.insubstantiate()
270