1# NixOS module for Buildbot continuous integration server.
2
3{ config, lib, options, pkgs, ... }:
4
5with lib;
6
7let
8 cfg = config.services.buildbot-master;
9 opt = options.services.buildbot-master;
10
11 package = pkgs.python3.pkgs.toPythonModule cfg.package;
12 python = package.pythonModule;
13
14 escapeStr = escape [ "'" ];
15
16 defaultMasterCfg = pkgs.writeText "master.cfg" ''
17 from buildbot.plugins import *
18 ${cfg.extraImports}
19 factory = util.BuildFactory()
20 c = BuildmasterConfig = dict(
21 workers = [${concatStringsSep "," cfg.workers}],
22 protocols = { 'pb': {'port': ${toString cfg.pbPort} } },
23 title = '${escapeStr cfg.title}',
24 titleURL = '${escapeStr cfg.titleUrl}',
25 buildbotURL = '${escapeStr cfg.buildbotUrl}',
26 db = dict(db_url='${escapeStr cfg.dbUrl}'),
27 www = dict(port=${toString cfg.port}),
28 change_source = [ ${concatStringsSep "," cfg.changeSource} ],
29 schedulers = [ ${concatStringsSep "," cfg.schedulers} ],
30 builders = [ ${concatStringsSep "," cfg.builders} ],
31 services = [ ${concatStringsSep "," cfg.reporters} ],
32 configurators = [ ${concatStringsSep "," cfg.configurators} ],
33 )
34 for step in [ ${concatStringsSep "," cfg.factorySteps} ]:
35 factory.addStep(step)
36
37 ${cfg.extraConfig}
38 '';
39
40 tacFile = pkgs.writeText "buildbot-master.tac" ''
41 import os
42
43 from twisted.application import service
44 from buildbot.master import BuildMaster
45
46 basedir = '${cfg.buildbotDir}'
47
48 configfile = '${cfg.masterCfg}'
49
50 # Default umask for server
51 umask = None
52
53 # note: this line is matched against to check that this is a buildmaster
54 # directory; do not edit it.
55 application = service.Application('buildmaster')
56
57 m = BuildMaster(basedir, configfile, umask)
58 m.setServiceParent(application)
59 '';
60
61in {
62 options = {
63 services.buildbot-master = {
64
65 factorySteps = mkOption {
66 type = types.listOf types.str;
67 description = "Factory Steps";
68 default = [];
69 example = [
70 "steps.Git(repourl='https://github.com/buildbot/pyflakes.git', mode='incremental')"
71 "steps.ShellCommand(command=['trial', 'pyflakes'])"
72 ];
73 };
74
75 changeSource = mkOption {
76 type = types.listOf types.str;
77 description = "List of Change Sources.";
78 default = [];
79 example = [
80 "changes.GitPoller('https://github.com/buildbot/pyflakes.git', workdir='gitpoller-workdir', branch='master', pollinterval=300)"
81 ];
82 };
83
84 configurators = mkOption {
85 type = types.listOf types.str;
86 description = "Configurator Steps, see https://docs.buildbot.net/latest/manual/configuration/configurators.html";
87 default = [];
88 example = [
89 "util.JanitorConfigurator(logHorizon=timedelta(weeks=4), hour=12, dayOfWeek=6)"
90 ];
91 };
92
93 enable = mkOption {
94 type = types.bool;
95 default = false;
96 description = "Whether to enable the Buildbot continuous integration server.";
97 };
98
99 extraConfig = mkOption {
100 type = types.str;
101 description = "Extra configuration to append to master.cfg";
102 default = "c['buildbotNetUsageData'] = None";
103 };
104
105 extraImports = mkOption {
106 type = types.str;
107 description = "Extra python imports to prepend to master.cfg";
108 default = "";
109 example = "from buildbot.process.project import Project";
110 };
111
112 masterCfg = mkOption {
113 type = types.path;
114 description = "Optionally pass master.cfg path. Other options in this configuration will be ignored.";
115 default = defaultMasterCfg;
116 defaultText = literalMD ''generated configuration file'';
117 example = "/etc/nixos/buildbot/master.cfg";
118 };
119
120 schedulers = mkOption {
121 type = types.listOf types.str;
122 description = "List of Schedulers.";
123 default = [
124 "schedulers.SingleBranchScheduler(name='all', change_filter=util.ChangeFilter(branch='master'), treeStableTimer=None, builderNames=['runtests'])"
125 "schedulers.ForceScheduler(name='force',builderNames=['runtests'])"
126 ];
127 };
128
129 builders = mkOption {
130 type = types.listOf types.str;
131 description = "List of Builders.";
132 default = [
133 "util.BuilderConfig(name='runtests',workernames=['example-worker'],factory=factory)"
134 ];
135 };
136
137 workers = mkOption {
138 type = types.listOf types.str;
139 description = "List of Workers.";
140 default = [ "worker.Worker('example-worker', 'pass')" ];
141 };
142
143 reporters = mkOption {
144 default = [];
145 type = types.listOf types.str;
146 description = "List of reporter objects used to present build status to various users.";
147 };
148
149 user = mkOption {
150 default = "buildbot";
151 type = types.str;
152 description = "User the buildbot server should execute under.";
153 };
154
155 group = mkOption {
156 default = "buildbot";
157 type = types.str;
158 description = "Primary group of buildbot user.";
159 };
160
161 extraGroups = mkOption {
162 type = types.listOf types.str;
163 default = [];
164 description = "List of extra groups that the buildbot user should be a part of.";
165 };
166
167 home = mkOption {
168 default = "/home/buildbot";
169 type = types.path;
170 description = "Buildbot home directory.";
171 };
172
173 buildbotDir = mkOption {
174 default = "${cfg.home}/master";
175 defaultText = literalExpression ''"''${config.${opt.home}}/master"'';
176 type = types.path;
177 description = "Specifies the Buildbot directory.";
178 };
179
180 pbPort = mkOption {
181 default = 9989;
182 type = types.either types.str types.int;
183 example = "'tcp:9990:interface=127.0.0.1'";
184 description = ''
185 The buildmaster will listen on a TCP port of your choosing
186 for connections from workers.
187 It can also use this port for connections from remote Change Sources,
188 status clients, and debug tools.
189 This port should be visible to the outside world, and you’ll need to tell
190 your worker admins about your choice.
191 If put in (single) quotes, this can also be used as a connection string,
192 as defined in the [ConnectionStrings guide](https://twistedmatrix.com/documents/current/core/howto/endpoints.html).
193 '';
194 };
195
196 listenAddress = mkOption {
197 default = "0.0.0.0";
198 type = types.str;
199 description = "Specifies the bind address on which the buildbot HTTP interface listens.";
200 };
201
202 buildbotUrl = mkOption {
203 default = "http://localhost:8010/";
204 type = types.str;
205 description = "Specifies the Buildbot URL.";
206 };
207
208 title = mkOption {
209 default = "Buildbot";
210 type = types.str;
211 description = "Specifies the Buildbot Title.";
212 };
213
214 titleUrl = mkOption {
215 default = "Buildbot";
216 type = types.str;
217 description = "Specifies the Buildbot TitleURL.";
218 };
219
220 dbUrl = mkOption {
221 default = "sqlite:///state.sqlite";
222 type = types.str;
223 description = "Specifies the database connection string.";
224 };
225
226 port = mkOption {
227 default = 8010;
228 type = types.port;
229 description = "Specifies port number on which the buildbot HTTP interface listens.";
230 };
231
232 package = mkPackageOption pkgs "buildbot-full" {
233 example = "buildbot";
234 };
235
236 packages = mkOption {
237 default = [ pkgs.git ];
238 defaultText = literalExpression "[ pkgs.git ]";
239 type = types.listOf types.package;
240 description = "Packages to add to PATH for the buildbot process.";
241 };
242
243 pythonPackages = mkOption {
244 type = types.functionTo (types.listOf types.package);
245 default = pythonPackages: with pythonPackages; [ ];
246 defaultText = literalExpression "pythonPackages: with pythonPackages; [ ]";
247 description = "Packages to add the to the PYTHONPATH of the buildbot process.";
248 example = literalExpression "pythonPackages: with pythonPackages; [ requests ]";
249 };
250 };
251 };
252
253 config = mkIf cfg.enable {
254 users.groups = optionalAttrs (cfg.group == "buildbot") {
255 buildbot = { };
256 };
257
258 users.users = optionalAttrs (cfg.user == "buildbot") {
259 buildbot = {
260 description = "Buildbot User.";
261 isNormalUser = true;
262 createHome = true;
263 inherit (cfg) home group extraGroups;
264 useDefaultShell = true;
265 };
266 };
267
268 systemd.services.buildbot-master = {
269 description = "Buildbot Continuous Integration Server.";
270 after = [ "network.target" ];
271 wantedBy = [ "multi-user.target" ];
272 path = cfg.packages ++ cfg.pythonPackages python.pkgs;
273 environment.PYTHONPATH = "${python.withPackages (self: cfg.pythonPackages self ++ [ package ])}/${python.sitePackages}";
274
275 preStart = ''
276 mkdir -vp "${cfg.buildbotDir}"
277 # Link the tac file so buildbot command line tools recognize the directory
278 ln -sf "${tacFile}" "${cfg.buildbotDir}/buildbot.tac"
279 ${cfg.package}/bin/buildbot create-master --db "${cfg.dbUrl}" "${cfg.buildbotDir}"
280 rm -f buildbot.tac.new master.cfg.sample
281 '';
282
283 serviceConfig = {
284 Type = "simple";
285 User = cfg.user;
286 Group = cfg.group;
287 WorkingDirectory = cfg.home;
288 # NOTE: call twistd directly with stdout logging for systemd
289 ExecStart = "${python.pkgs.twisted}/bin/twistd -o --nodaemon --pidfile= --logfile - --python ${cfg.buildbotDir}/buildbot.tac";
290 # To reload on upgrade, set the following in your configuration:
291 # systemd.services.buildbot-master.reloadIfChanged = true;
292 ExecReload = [
293 "${pkgs.coreutils}/bin/ln -sf ${tacFile} ${cfg.buildbotDir}/buildbot.tac"
294 "${pkgs.coreutils}/bin/kill -HUP $MAINPID"
295 ];
296 };
297 };
298 };
299
300 imports = [
301 (mkRenamedOptionModule [ "services" "buildbot-master" "bpPort" ] [ "services" "buildbot-master" "pbPort" ])
302 (mkRemovedOptionModule [ "services" "buildbot-master" "status" ] ''
303 Since Buildbot 0.9.0, status targets are deprecated and ignored.
304 Review your configuration and migrate to reporters (available at services.buildbot-master.reporters).
305 '')
306 ];
307
308 meta.maintainers = lib.teams.buildbot.members;
309}