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 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
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
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