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 youll 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}