aboutsummaryrefslogtreecommitdiff
path: root/modules/private/buildbot/common/libvirt.py
blob: e25062744ca2ec5e332f711770d35431ea80b6a6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# This file was part of Buildbot.  Buildbot is free software: you can
# redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Portions Copyright Buildbot Team Members
# Portions Copyright 2010 Isotoma Limited


import os

from twisted.internet import defer
from twisted.internet import threads
from twisted.internet import utils
from twisted.python import failure
from twisted.python import log

from buildbot import config
from buildbot.util.eventual import eventually
from buildbot.worker import AbstractLatentWorker

try:
    import libvirt
except ImportError:
    libvirt = None

import random
import string

def random_string_generator():
    chars = string.ascii_letters
    return ''.join(random.choice(chars) for x in range(6))

class WorkQueue:

    """
    I am a class that turns parallel access into serial access.

    I exist because we want to run libvirt access in threads as we don't
    trust calls not to block, but under load libvirt doesn't seem to like
    this kind of threaded use.
    """

    def __init__(self):
        self.queue = []

    def _process(self):
        log.msg("Looking to start a piece of work now...")

        # Is there anything to do?
        if not self.queue:
            log.msg("_process called when there is no work")
            return

        # Peek at the top of the stack - get a function to call and
        # a deferred to fire when its all over
        d, next_operation, args, kwargs = self.queue[0]

        # Start doing some work - expects a deferred
        try:
            d2 = next_operation(*args, **kwargs)
        except Exception:
            d2 = defer.fail()

        # Whenever a piece of work is done, whether it worked or not
        # call this to schedule the next piece of work
        @d2.addBoth
        def _work_done(res):
            log.msg("Completed a piece of work")
            self.queue.pop(0)
            if self.queue:
                log.msg("Preparing next piece of work")
                eventually(self._process)
            return res

        # When the work is done, trigger d
        d2.chainDeferred(d)

    def execute(self, cb, *args, **kwargs):
        kickstart_processing = not self.queue
        d = defer.Deferred()
        self.queue.append((d, cb, args, kwargs))
        if kickstart_processing:
            self._process()
        return d

    def executeInThread(self, cb, *args, **kwargs):
        return self.execute(threads.deferToThread, cb, *args, **kwargs)


# A module is effectively a singleton class, so this is OK
queue = WorkQueue()


class Domain:

    """
    I am a wrapper around a libvirt Domain object
    """

    def __init__(self, connection, domain):
        self.connection = connection
        self.domain = domain

    def name(self):
        return queue.executeInThread(self.domain.name)

    def create(self):
        return queue.executeInThread(self.domain.create)

    def shutdown(self):
        return queue.executeInThread(self.domain.shutdown)

    def destroy(self):
        return queue.executeInThread(self.domain.destroy)

class Volume:
    def __init__(self, connection, volume):
        self.connection = connection
        self.volume = volume

    @defer.inlineCallbacks
    def destroy(self):
        yield queue.executeInThread(self.volume.wipe)
        yield queue.executeInThread(self.volume.delete)

class Pool:
    VolumeClass = Volume
    def __init__(self, connection, pool):
        self.connection = connection
        self.pool = pool

    @defer.inlineCallbacks
    def create_volume(self, xml):
        res = yield queue.executeInThread(self.pool.createXML, xml)
        return self.VolumeClass(self.connection, res)

class Connection:

    """
    I am a wrapper around a libvirt Connection object.
    """

    DomainClass = Domain
    PoolClass = Pool

    def __init__(self, uri):
        self.uri = uri
        self._connection = None

    @property
    def connection(self):
        if self._connection is not None:
            try:
                if not self._connection.isAlive():
                    self._connection = None
            except:
                self._connection = None
        if self._connection is None:
            self._connection = libvirt.open(self.uri)
        return self._connection

    @defer.inlineCallbacks
    def create(self, xml):
        """ I take libvirt XML and start a new VM """
        res = yield queue.executeInThread(self.connection.createXML, xml, 0)
        return self.DomainClass(self, res)

    @defer.inlineCallbacks
    def lookup_pool(self, name):
        res = yield queue.executeInThread(self.connection.storagePoolLookupByName, name)
        return self.PoolClass(self, res)

class LibVirtWorker(AbstractLatentWorker):

    def __init__(self, name, password, connection, master_url, base_image=None, **kwargs):
        super().__init__(name, password, **kwargs)
        if not libvirt:
            config.error(
                "The python module 'libvirt' is needed to use a LibVirtWorker")

        self.master_url = master_url
        self.random_name = random_string_generator()
        self.connection = connection
        self.base_image = base_image

        self.domain = None
        self.domain_name = "buildbot-" + self.workername + "-" + self.random_name
        self.volume = None
        self.volume_name = "buildbot-" + self.workername + "-" + self.random_name
        self.pool_name = "buildbot-disks"

    def reconfigService(self, *args, **kwargs):
        if 'build_wait_timeout' not in kwargs:
            kwargs['build_wait_timeout'] = 0
        return super().reconfigService(*args, **kwargs)

    def canStartBuild(self):
        if self.domain and not self.isConnected():
            log.msg(
                "Not accepting builds as existing domain but worker not connected")
            return False

        return super().canStartBuild()

    @defer.inlineCallbacks
    def _prepare_image(self):
        log.msg("Creating temporary image {}".format(self.volume_name))
        pool = yield self.connection.lookup_pool(self.pool_name)
        vol_xml = """
            <volume type='file'>
                <name>{vol_name}</name>
                <capacity unit='G'>10</capacity>
                <target>
                    <format type='qcow2'/>
                    <permissions>
                        <mode>0600</mode>
                        <owner>0</owner>
                        <group>0</group>
                    </permissions>
                </target>
                <backingStore>
                    <path>/etc/libvirtd/base-images/buildbot.qcow2</path>
                    <format type='qcow2'/>
                </backingStore>
            </volume>
        """.format(vol_name = self.volume_name)
        self.volume = yield pool.create_volume(vol_xml)

    @defer.inlineCallbacks
    def start_instance(self, build):
        """
        I start a new instance of a VM.

        If a base_image is specified, I will make a clone of that otherwise i will
        use image directly.

        If i'm not given libvirt domain definition XML, I will look for my name
        in the list of defined virtual machines and start that.
        """
        domain_xml = """
            <domain type="kvm">
                <name>{domain_name}</name>
                <memory unit="GiB">2</memory>
                <vcpu>1</vcpu>
                <sysinfo type='smbios'>
                    <oemStrings>
                        <entry>buildbot_master_url={master_url}</entry>
                        <entry>buildbot_worker_name={worker_name}</entry>
                    </oemStrings>
                </sysinfo>
                <os>
                    <type arch="x86_64">hvm</type>
                    <smbios mode='sysinfo'/>
                </os>
                <devices>
                    <emulator>/run/current-system/sw/bin/qemu-system-x86_64</emulator>
                    <disk type="volume" device="disk">
                        <driver name='qemu' type='qcow2' />
                        <source type="volume" pool="{pool_name}" volume="{volume_name}" />
                        <backingStore type='volume'>
                        <format type='qcow2'/>
                        <source type="volume" pool="niximages" volume="buildbot.qcow2" />
                        </backingStore>
                        <target dev="vda" bus="virtio"/>
                    </disk>
                    <input type="keyboard" bus="usb"/>
                    <graphics type="vnc" port="-1" autoport="yes"/>
                    <interface type="network">
                        <source network="immae" />
                    </interface>
                </devices>
            </domain>
        """.format(volume_name = self.volume_name, master_url = self.master_url, pool_name =
                self.pool_name, domain_name = self.domain_name, worker_name = self.workername)

        yield self._prepare_image()

        try:
            self.domain = yield self.connection.create(domain_xml)
        except Exception:
            log.err(failure.Failure(),
                    ("Cannot start a VM ({}), failing gracefully and triggering"
                     "a new build check").format(self.workername))
            self.domain = None
            return False

        return [self.domain_name]

    def stop_instance(self, fast=False):
        """
        I attempt to stop a running VM.
        I make sure any connection to the worker is removed.
        If the VM was using a cloned image, I remove the clone
        When everything is tidied up, I ask that bbot looks for work to do
        """

        log.msg("Attempting to stop '{}'".format(self.workername))
        if self.domain is None:
            log.msg("I don't think that domain is even running, aborting")
            return defer.succeed(None)

        domain = self.domain
        self.domain = None

        d = domain.destroy()
        if self.volume is not None:
            self.volume.destroy()

        return d