1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
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
37
39 log.msg("Looking to start a piece of work now...")
40
41
42 if not self.queue:
43 log.msg("_process called when there is no work")
44 return
45
46
47
48 d, next_operation, args, kwargs = self.queue[0]
49
50
51 try:
52 d2 = next_operation(*args, **kwargs)
53 except:
54 d2 = defer.fail()
55
56
57
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
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
79 return self.execute(threads.deferToThread, cb, *args, **kwargs)
80
81
82
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
98
101
104
105
107
108 """
109 I am a wrapper around a libvirt Connection object.
110 """
111
113 self.uri = uri
114 self.connection = libvirt.open(uri)
115
121 d.addCallback(_)
122 return d
123
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
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
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
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
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
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