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