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 return self.connection.create(self.xml) 199 d = self.connection.lookupByName(self.name) 200 def _really_start(res): 201 return res.create()
202 d.addCallback(_really_start) 203 return d 204 d.addCallback(_start) 205 206 def _started(res): 207 self.domain = res 208 return True 209 d.addCallback(_started) 210 211 def _start_failed(failure): 212 log.msg("Cannot start a VM (%s), failing gracefully and triggering a new build check" % self.name) 213 log.err(failure) 214 self.domain = None 215 return False 216 d.addErrback(_start_failed) 217 218 return d 219
220 - def stop_instance(self, fast=False):
221 """ 222 I attempt to stop a running VM. 223 I make sure any connection to the slave is removed. 224 If the VM was using a cloned image, I remove the clone 225 When everything is tidied up, I ask that bbot looks for work to do 226 """ 227 log.msg("Attempting to stop '%s'" % self.name) 228 if self.domain is None: 229 log.msg("I don't think that domain is evening running, aborting") 230 return defer.succeed(None) 231 232 domain = self.domain 233 self.domain = None 234 235 if self.graceful_shutdown and not fast: 236 log.msg("Graceful shutdown chosen for %s" % self.name) 237 d = domain.shutdown() 238 else: 239 d = domain.destroy() 240 241 def _disconnect(res): 242 log.msg("VM destroyed (%s): Forcing its connection closed." % self.name) 243 return AbstractBuildSlave.disconnect(self)
244 d.addCallback(_disconnect) 245 246 def _disconnected(res): 247 log.msg("We forced disconnection (%s), cleaning up and triggering new build" % self.name) 248 if self.base_image: 249 os.remove(self.image) 250 self.botmaster.maybeStartBuildsForSlave(self.name) 251 return res 252 d.addBoth(_disconnected) 253 254 return d 255
256 - def buildFinished(self, *args, **kwargs):
257 """ 258 I insubstantiate a slave after it has done a build, if that is 259 desired behaviour. 260 """ 261 AbstractLatentBuildSlave.buildFinished(self, *args, **kwargs) 262 if self.insubstantiate_after_build: 263 log.msg("Got buildFinished notification - attempting to insubstantiate") 264 self.insubstantiate()
265