aboutsummaryrefslogtreecommitdiff
path: root/modules/private/buildbot/common
diff options
context:
space:
mode:
Diffstat (limited to 'modules/private/buildbot/common')
-rw-r--r--modules/private/buildbot/common/build_helpers.py277
-rw-r--r--modules/private/buildbot/common/libvirt.py318
-rw-r--r--modules/private/buildbot/common/master.cfg69
3 files changed, 0 insertions, 664 deletions
diff --git a/modules/private/buildbot/common/build_helpers.py b/modules/private/buildbot/common/build_helpers.py
deleted file mode 100644
index acea905..0000000
--- a/modules/private/buildbot/common/build_helpers.py
+++ /dev/null
@@ -1,277 +0,0 @@
1from buildbot.plugins import util, steps, schedulers
2from buildbot_buildslist import BuildsList
3from shutil import which
4
5__all__ = [
6 "force_scheduler", "deploy_scheduler", "hook_scheduler",
7 "clean_branch", "package_and_upload", "SlackStatusPush",
8 "XMPPStatusPush", "NixShellCommand"
9 ]
10
11# Small helpers"
12@util.renderer
13def clean_branch(props):
14 if props.hasProperty("branch") and len(props["branch"]) > 0:
15 return props["branch"].replace("/", "_")
16 else:
17 return "HEAD"
18
19def package_and_upload(package, package_dest, package_url):
20 return [
21 steps.ShellCommand(name="build package",
22 logEnviron=False, haltOnFailure=True, workdir="source",
23 command=["git", "archive", "HEAD", "-o", package]),
24
25 steps.FileUpload(name="upload package", workersrc=package,
26 workdir="source", masterdest=package_dest,
27 url=package_url, mode=0o644),
28
29 steps.ShellCommand(name="cleanup package", logEnviron=False,
30 haltOnFailure=True, workdir="source", alwaysRun=True,
31 command=["rm", "-f", package]),
32 ]
33
34# Steps
35class NixShellCommand(steps.ShellCommand):
36 def __init__(self, command=None, pure=True, nixfile=None, **kwargs):
37 assert(isinstance(command, str))
38 oldpath = kwargs.get("env", {}).get("PATH", None)
39 if which("nix-shell", path=oldpath) is None:
40 kwargs["env"] = kwargs.get("env", {})
41 if isinstance(oldpath, str):
42 kwargs["env"]["PATH"] = "/run/current-system/sw/bin:" + oldpath
43 elif isinstance(oldpath, list):
44 kwargs["env"]["PATH"] = ["/run/current-system/sw/bin"] + oldpath
45 nixcommand = ["nix-shell"]
46 if pure:
47 nixcommand.append("--pure")
48 nixcommand.append("--run")
49 nixcommand.append(command)
50 if nixfile is not None:
51 nixcommand.append(nixfile)
52 super().__init__(command=nixcommand, **kwargs)
53
54# Schedulers
55def force_scheduler(name, builders):
56 return schedulers.ForceScheduler(name=name,
57 label="Force build", buttonName="Force build",
58 reason=util.StringParameter(name="reason", label="Reason", default="Force build"),
59 codebases=[
60 util.CodebaseParameter("",
61 branch=util.StringParameter(
62 name="branch", label="Git reference (tag, branch)", required=True),
63 revision=util.FixedParameter(name="revision", default=""),
64 repository=util.FixedParameter(name="repository", default=""),
65 project=util.FixedParameter(name="project", default=""),
66 ),
67 ],
68 username=util.FixedParameter(name="username", default="Web button"),
69 builderNames=builders)
70
71def deploy_scheduler(name, builders):
72 return schedulers.ForceScheduler(name=name,
73 builderNames=builders,
74 label="Deploy built package", buttonName="Deploy",
75 username=util.FixedParameter(name="username", default="Web button"),
76 codebases=[
77 util.CodebaseParameter(codebase="",
78 branch=util.FixedParameter(name="branch", default=""),
79 revision=util.FixedParameter(name="revision", default=""),
80 repository=util.FixedParameter(name="repository", default=""),
81 project=util.FixedParameter(name="project", default=""))],
82 reason=util.FixedParameter(name="reason", default="Deploy"),
83 properties=[
84 util.ChoiceStringParameter(label="Environment",
85 name="environment", default="integration",
86 choices=["integration", "production"]),
87 BuildsList(label="Build to deploy", name="build"),
88 ]
89 )
90
91def hook_scheduler(project, timer=10):
92 return schedulers.AnyBranchScheduler(
93 change_filter=util.ChangeFilter(category="hooks", project=project),
94 name=project, treeStableTimer=timer, builderNames=["{}_build".format(project)])
95
96# Slack/XMPP status push
97from buildbot.reporters.http import HttpStatusPushBase
98from twisted.internet import defer
99from twisted.python import log
100from buildbot.util import httpclientservice
101from buildbot.reporters import utils
102from buildbot.process import results
103from twisted.words.protocols.jabber.jid import JID
104from wokkel import client, xmppim
105from functools import partial
106
107class SlackStatusPush(HttpStatusPushBase):
108 name = "SlackStatusPush"
109
110 @defer.inlineCallbacks
111 def reconfigService(self, serverUrl, **kwargs):
112 yield HttpStatusPushBase.reconfigService(self, **kwargs)
113 self._http = yield httpclientservice.HTTPClientService.getService(
114 self.master, serverUrl)
115
116 @defer.inlineCallbacks
117 def send(self, build):
118 yield utils.getDetailsForBuild(self.master, build, wantProperties=True)
119 response = yield self._http.post("", json=self.format(build))
120 if response.code != 200:
121 log.msg("%s: unable to upload status: %s" %
122 (response.code, response.content))
123
124 def format(self, build):
125 colors = [
126 "#36A64F", # success
127 "#F1E903", # warnings
128 "#DA0505", # failure
129 "#FFFFFF", # skipped
130 "#000000", # exception
131 "#FFFFFF", # retry
132 "#D02CA9", # cancelled
133 ]
134
135 if "environment" in build["properties"]:
136 msg = "{} environment".format(build["properties"]["environment"][0])
137 if "build" in build["properties"]:
138 msg = "of archive {} in ".format(build["properties"]["build"][0]) + msg
139 elif len(build["buildset"]["sourcestamps"][0]["branch"] or []) > 0:
140 msg = "revision {}".format(build["buildset"]["sourcestamps"][0]["branch"])
141 else:
142 msg = "build"
143
144 if build["complete"]:
145 timedelta = int((build["complete_at"] - build["started_at"]).total_seconds())
146 hours, rest = divmod(timedelta, 3600)
147 minutes, seconds = divmod(rest, 60)
148 if hours > 0:
149 duration = "{}h {}min {}s".format(hours, minutes, seconds)
150 elif minutes > 0:
151 duration = "{}min {}s".format(minutes, seconds)
152 else:
153 duration = "{}s".format(seconds)
154
155 text = "Build <{}|{}> of {}'s {} was {} in {}.".format(
156 build["url"], build["buildid"],
157 build["builder"]["name"],
158 msg,
159 results.Results[build["results"]],
160 duration,
161 )
162 fields = [
163 {
164 "title": "Build",
165 "value": "<{}|{}>".format(build["url"], build["buildid"]),
166 "short": True,
167 },
168 {
169 "title": "Project",
170 "value": build["builder"]["name"],
171 "short": True,
172 },
173 {
174 "title": "Build status",
175 "value": results.Results[build["results"]],
176 "short": True,
177 },
178 {
179 "title": "Build duration",
180 "value": duration,
181 "short": True,
182 },
183 ]
184 if "environment" in build["properties"]:
185 fields.append({
186 "title": "Environment",
187 "value": build["properties"]["environment"][0],
188 "short": True,
189 })
190 if "build" in build["properties"]:
191 fields.append({
192 "title": "Archive",
193 "value": build["properties"]["build"][0],
194 "short": True,
195 })
196 attachments = [{
197 "fallback": "",
198 "color": colors[build["results"]],
199 "fields": fields
200 }]
201 else:
202 text = "Build <{}|{}> of {}'s {} started.".format(
203 build["url"], build["buildid"],
204 build["builder"]["name"],
205 msg,
206 )
207 attachments = []
208
209 return {
210 "username": "Buildbot",
211 "icon_url": "http://docs.buildbot.net/current/_static/icon.png",
212 "text": text,
213 "attachments": attachments,
214 }
215
216class XMPPStatusPush(HttpStatusPushBase):
217 name = "XMPPStatusPush"
218
219 @defer.inlineCallbacks
220 def reconfigService(self, password, recipients, **kwargs):
221 yield HttpStatusPushBase.reconfigService(self, **kwargs)
222 self.password = password
223 self.recipients = recipients
224
225 @defer.inlineCallbacks
226 def send(self, build):
227 yield utils.getDetailsForBuild(self.master, build, wantProperties=True)
228 body = self.format(build)
229 factory = client.DeferredClientFactory(JID("notify_bot@immae.fr/buildbot"), self.password)
230 d = client.clientCreator(factory)
231 def send_message(recipient, stream):
232 message = xmppim.Message(recipient=JID(recipient), body=body)
233 message.stanzaType = 'chat'
234 stream.send(message.toElement())
235 # To allow chaining
236 return stream
237 for recipient in self.recipients:
238 d.addCallback(partial(send_message, recipient))
239 d.addCallback(lambda _: factory.streamManager.xmlstream.sendFooter())
240 d.addErrback(log.err)
241
242 def format(self, build):
243 if "environment" in build["properties"]:
244 msg = "{} environment".format(build["properties"]["environment"][0])
245 if "build" in build["properties"]:
246 msg = "of archive {} in ".format(build["properties"]["build"][0]) + msg
247 elif len(build["buildset"]["sourcestamps"][0]["branch"] or []) > 0:
248 msg = "revision {}".format(build["buildset"]["sourcestamps"][0]["branch"])
249 else:
250 msg = "build"
251
252 if build["complete"]:
253 timedelta = int((build["complete_at"] - build["started_at"]).total_seconds())
254 hours, rest = divmod(timedelta, 3600)
255 minutes, seconds = divmod(rest, 60)
256 if hours > 0:
257 duration = "{}h {}min {}s".format(hours, minutes, seconds)
258 elif minutes > 0:
259 duration = "{}min {}s".format(minutes, seconds)
260 else:
261 duration = "{}s".format(seconds)
262
263 text = "Build {} ( {} ) of {}'s {} was {} in {}.".format(
264 build["buildid"], build["url"],
265 build["builder"]["name"],
266 msg,
267 results.Results[build["results"]],
268 duration,
269 )
270 else:
271 text = "Build {} ( {} ) of {}'s {} started.".format(
272 build["buildid"], build["url"],
273 build["builder"]["name"],
274 msg,
275 )
276
277 return text
diff --git a/modules/private/buildbot/common/libvirt.py b/modules/private/buildbot/common/libvirt.py
deleted file mode 100644
index e250627..0000000
--- a/modules/private/buildbot/common/libvirt.py
+++ /dev/null
@@ -1,318 +0,0 @@
1# This file was 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
18import os
19
20from twisted.internet import defer
21from twisted.internet import threads
22from twisted.internet import utils
23from twisted.python import failure
24from twisted.python import log
25
26from buildbot import config
27from buildbot.util.eventual import eventually
28from buildbot.worker import AbstractLatentWorker
29
30try:
31 import libvirt
32except ImportError:
33 libvirt = None
34
35import random
36import string
37
38def random_string_generator():
39 chars = string.ascii_letters
40 return ''.join(random.choice(chars) for x in range(6))
41
42class WorkQueue:
43
44 """
45 I am a class that turns parallel access into serial access.
46
47 I exist because we want to run libvirt access in threads as we don't
48 trust calls not to block, but under load libvirt doesn't seem to like
49 this kind of threaded use.
50 """
51
52 def __init__(self):
53 self.queue = []
54
55 def _process(self):
56 log.msg("Looking to start a piece of work now...")
57
58 # Is there anything to do?
59 if not self.queue:
60 log.msg("_process called when there is no work")
61 return
62
63 # Peek at the top of the stack - get a function to call and
64 # a deferred to fire when its all over
65 d, next_operation, args, kwargs = self.queue[0]
66
67 # Start doing some work - expects a deferred
68 try:
69 d2 = next_operation(*args, **kwargs)
70 except Exception:
71 d2 = defer.fail()
72
73 # Whenever a piece of work is done, whether it worked or not
74 # call this to schedule the next piece of work
75 @d2.addBoth
76 def _work_done(res):
77 log.msg("Completed a piece of work")
78 self.queue.pop(0)
79 if self.queue:
80 log.msg("Preparing next piece of work")
81 eventually(self._process)
82 return res
83
84 # When the work is done, trigger d
85 d2.chainDeferred(d)
86
87 def execute(self, cb, *args, **kwargs):
88 kickstart_processing = not self.queue
89 d = defer.Deferred()
90 self.queue.append((d, cb, args, kwargs))
91 if kickstart_processing:
92 self._process()
93 return d
94
95 def executeInThread(self, cb, *args, **kwargs):
96 return self.execute(threads.deferToThread, cb, *args, **kwargs)
97
98
99# A module is effectively a singleton class, so this is OK
100queue = WorkQueue()
101
102
103class Domain:
104
105 """
106 I am a wrapper around a libvirt Domain object
107 """
108
109 def __init__(self, connection, domain):
110 self.connection = connection
111 self.domain = domain
112
113 def name(self):
114 return queue.executeInThread(self.domain.name)
115
116 def create(self):
117 return queue.executeInThread(self.domain.create)
118
119 def shutdown(self):
120 return queue.executeInThread(self.domain.shutdown)
121
122 def destroy(self):
123 return queue.executeInThread(self.domain.destroy)
124
125class Volume:
126 def __init__(self, connection, volume):
127 self.connection = connection
128 self.volume = volume
129
130 @defer.inlineCallbacks
131 def destroy(self):
132 yield queue.executeInThread(self.volume.wipe)
133 yield queue.executeInThread(self.volume.delete)
134
135class Pool:
136 VolumeClass = Volume
137 def __init__(self, connection, pool):
138 self.connection = connection
139 self.pool = pool
140
141 @defer.inlineCallbacks
142 def create_volume(self, xml):
143 res = yield queue.executeInThread(self.pool.createXML, xml)
144 return self.VolumeClass(self.connection, res)
145
146class Connection:
147
148 """
149 I am a wrapper around a libvirt Connection object.
150 """
151
152 DomainClass = Domain
153 PoolClass = Pool
154
155 def __init__(self, uri):
156 self.uri = uri
157 self._connection = None
158
159 @property
160 def connection(self):
161 if self._connection is not None:
162 try:
163 if not self._connection.isAlive():
164 self._connection = None
165 except:
166 self._connection = None
167 if self._connection is None:
168 self._connection = libvirt.open(self.uri)
169 return self._connection
170
171 @defer.inlineCallbacks
172 def create(self, xml):
173 """ I take libvirt XML and start a new VM """
174 res = yield queue.executeInThread(self.connection.createXML, xml, 0)
175 return self.DomainClass(self, res)
176
177 @defer.inlineCallbacks
178 def lookup_pool(self, name):
179 res = yield queue.executeInThread(self.connection.storagePoolLookupByName, name)
180 return self.PoolClass(self, res)
181
182class LibVirtWorker(AbstractLatentWorker):
183
184 def __init__(self, name, password, connection, master_url, base_image=None, **kwargs):
185 super().__init__(name, password, **kwargs)
186 if not libvirt:
187 config.error(
188 "The python module 'libvirt' is needed to use a LibVirtWorker")
189
190 self.master_url = master_url
191 self.random_name = random_string_generator()
192 self.connection = connection
193 self.base_image = base_image
194
195 self.domain = None
196 self.domain_name = "buildbot-" + self.workername + "-" + self.random_name
197 self.volume = None
198 self.volume_name = "buildbot-" + self.workername + "-" + self.random_name
199 self.pool_name = "buildbot-disks"
200
201 def reconfigService(self, *args, **kwargs):
202 if 'build_wait_timeout' not in kwargs:
203 kwargs['build_wait_timeout'] = 0
204 return super().reconfigService(*args, **kwargs)
205
206 def canStartBuild(self):
207 if self.domain and not self.isConnected():
208 log.msg(
209 "Not accepting builds as existing domain but worker not connected")
210 return False
211
212 return super().canStartBuild()
213
214 @defer.inlineCallbacks
215 def _prepare_image(self):
216 log.msg("Creating temporary image {}".format(self.volume_name))
217 pool = yield self.connection.lookup_pool(self.pool_name)
218 vol_xml = """
219 <volume type='file'>
220 <name>{vol_name}</name>
221 <capacity unit='G'>10</capacity>
222 <target>
223 <format type='qcow2'/>
224 <permissions>
225 <mode>0600</mode>
226 <owner>0</owner>
227 <group>0</group>
228 </permissions>
229 </target>
230 <backingStore>
231 <path>/etc/libvirtd/base-images/buildbot.qcow2</path>
232 <format type='qcow2'/>
233 </backingStore>
234 </volume>
235 """.format(vol_name = self.volume_name)
236 self.volume = yield pool.create_volume(vol_xml)
237
238 @defer.inlineCallbacks
239 def start_instance(self, build):
240 """
241 I start a new instance of a VM.
242
243 If a base_image is specified, I will make a clone of that otherwise i will
244 use image directly.
245
246 If i'm not given libvirt domain definition XML, I will look for my name
247 in the list of defined virtual machines and start that.
248 """
249 domain_xml = """
250 <domain type="kvm">
251 <name>{domain_name}</name>
252 <memory unit="GiB">2</memory>
253 <vcpu>1</vcpu>
254 <sysinfo type='smbios'>
255 <oemStrings>
256 <entry>buildbot_master_url={master_url}</entry>
257 <entry>buildbot_worker_name={worker_name}</entry>
258 </oemStrings>
259 </sysinfo>
260 <os>
261 <type arch="x86_64">hvm</type>
262 <smbios mode='sysinfo'/>
263 </os>
264 <devices>
265 <emulator>/run/current-system/sw/bin/qemu-system-x86_64</emulator>
266 <disk type="volume" device="disk">
267 <driver name='qemu' type='qcow2' />
268 <source type="volume" pool="{pool_name}" volume="{volume_name}" />
269 <backingStore type='volume'>
270 <format type='qcow2'/>
271 <source type="volume" pool="niximages" volume="buildbot.qcow2" />
272 </backingStore>
273 <target dev="vda" bus="virtio"/>
274 </disk>
275 <input type="keyboard" bus="usb"/>
276 <graphics type="vnc" port="-1" autoport="yes"/>
277 <interface type="network">
278 <source network="immae" />
279 </interface>
280 </devices>
281 </domain>
282 """.format(volume_name = self.volume_name, master_url = self.master_url, pool_name =
283 self.pool_name, domain_name = self.domain_name, worker_name = self.workername)
284
285 yield self._prepare_image()
286
287 try:
288 self.domain = yield self.connection.create(domain_xml)
289 except Exception:
290 log.err(failure.Failure(),
291 ("Cannot start a VM ({}), failing gracefully and triggering"
292 "a new build check").format(self.workername))
293 self.domain = None
294 return False
295
296 return [self.domain_name]
297
298 def stop_instance(self, fast=False):
299 """
300 I attempt to stop a running VM.
301 I make sure any connection to the worker is removed.
302 If the VM was using a cloned image, I remove the clone
303 When everything is tidied up, I ask that bbot looks for work to do
304 """
305
306 log.msg("Attempting to stop '{}'".format(self.workername))
307 if self.domain is None:
308 log.msg("I don't think that domain is even running, aborting")
309 return defer.succeed(None)
310
311 domain = self.domain
312 self.domain = None
313
314 d = domain.destroy()
315 if self.volume is not None:
316 self.volume.destroy()
317
318 return d
diff --git a/modules/private/buildbot/common/master.cfg b/modules/private/buildbot/common/master.cfg
deleted file mode 100644
index abe08e0..0000000
--- a/modules/private/buildbot/common/master.cfg
+++ /dev/null
@@ -1,69 +0,0 @@
1# -*- python -*-
2# ex: set filetype=python:
3
4from buildbot.plugins import secrets, util, webhooks
5from buildbot.util import bytes2unicode
6import re
7import os
8from buildbot_config import E, configure
9import json
10
11class CustomBase(webhooks.base):
12 def getChanges(self, request):
13 try:
14 content = request.content.read()
15 args = json.loads(bytes2unicode(content))
16 except Exception as e:
17 raise ValueError("Error loading JSON: " + str(e))
18
19 args.setdefault("comments", "")
20 args.setdefault("repository", "")
21 args.setdefault("author", args.get("who"))
22
23 return ([args], None)
24
25userInfoProvider = util.LdapUserInfo(
26 uri=E.LDAP_URL,
27 bindUser=E.LDAP_ADMIN_USER,
28 bindPw=open(E.SECRETS_FILE + "/ldap", "r").read().rstrip(),
29 accountBase=E.LDAP_BASE,
30 accountPattern=E.LDAP_PATTERN,
31 accountFullName='cn',
32 accountEmail='mail',
33 avatarData="jpegPhoto",
34 groupBase=E.LDAP_BASE,
35 groupName="cn",
36 groupMemberPattern=E.LDAP_GROUP_PATTERN,
37 )
38
39c = BuildmasterConfig = {
40 "title": E.TITLE,
41 "titleURL": E.TITLE_URL,
42 "db": {
43 "db_url": "sqlite:///state.sqlite"
44 },
45 "protocols": { "pb": { "port": E.PB_SOCKET } },
46 "workers": [],
47 "change_source": [],
48 "schedulers": [],
49 "builders": [],
50 "services": [],
51 "secretsProviders": [
52 secrets.SecretInAFile(E.SECRETS_FILE),
53 ],
54 "www": {
55 "change_hook_dialects": { "base": { "custom_class": CustomBase } },
56 "plugins": {
57 "waterfall_view": {},
58 "console_view": {},
59 "grid_view": {},
60 "buildslist": {},
61 },
62 "auth": util.RemoteUserAuth(
63 header=b"X-Remote-User",
64 userInfoProvider=userInfoProvider,
65 headerRegex=re.compile(br"(?P<username>[^ @]+)")),
66 }
67 }
68
69configure(c)