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