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