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 example = "/etc/nixos/buildbot/master.cfg"; 97 }; 98 99 schedulers = mkOption { 100 type = types.listOf types.str; 101 description = "List of Schedulers."; 102 default = [ 103 "schedulers.SingleBranchScheduler(name='all', change_filter=util.ChangeFilter(branch='master'), treeStableTimer=None, builderNames=['runtests'])" 104 "schedulers.ForceScheduler(name='force',builderNames=['runtests'])" 105 ]; 106 }; 107 108 builders = mkOption { 109 type = types.listOf types.str; 110 description = "List of Builders."; 111 default = [ 112 "util.BuilderConfig(name='runtests',workernames=['example-worker'],factory=factory)" 113 ]; 114 }; 115 116 workers = mkOption { 117 type = types.listOf types.str; 118 description = "List of Workers."; 119 default = [ "worker.Worker('example-worker', 'pass')" ]; 120 }; 121 122 reporters = mkOption { 123 default = []; 124 type = types.listOf types.str; 125 description = "List of reporter objects used to present build status to various users."; 126 }; 127 128 user = mkOption { 129 default = "buildbot"; 130 type = types.str; 131 description = "User the buildbot server should execute under."; 132 }; 133 134 group = mkOption { 135 default = "buildbot"; 136 type = types.str; 137 description = "Primary group of buildbot user."; 138 }; 139 140 extraGroups = mkOption { 141 type = types.listOf types.str; 142 default = []; 143 description = "List of extra groups that the buildbot user should be a part of."; 144 }; 145 146 home = mkOption { 147 default = "/home/buildbot"; 148 type = types.path; 149 description = "Buildbot home directory."; 150 }; 151 152 buildbotDir = mkOption { 153 default = "${cfg.home}/master"; 154 type = types.path; 155 description = "Specifies the Buildbot directory."; 156 }; 157 158 pbPort = mkOption { 159 default = 9989; 160 type = types.either types.str types.int; 161 example = "'tcp:9990:interface=127.0.0.1'"; 162 description = '' 163 The buildmaster will listen on a TCP port of your choosing 164 for connections from workers. 165 It can also use this port for connections from remote Change Sources, 166 status clients, and debug tools. 167 This port should be visible to the outside world, and youll need to tell 168 your worker admins about your choice. 169 If put in (single) quotes, this can also be used as a connection string, 170 as defined in the <link xlink:href="https://twistedmatrix.com/documents/current/core/howto/endpoints.html">ConnectionStrings guide</link>. 171 ''; 172 }; 173 174 listenAddress = mkOption { 175 default = "0.0.0.0"; 176 type = types.str; 177 description = "Specifies the bind address on which the buildbot HTTP interface listens."; 178 }; 179 180 buildbotUrl = mkOption { 181 default = "http://localhost:8010/"; 182 type = types.str; 183 description = "Specifies the Buildbot URL."; 184 }; 185 186 title = mkOption { 187 default = "Buildbot"; 188 type = types.str; 189 description = "Specifies the Buildbot Title."; 190 }; 191 192 titleUrl = mkOption { 193 default = "Buildbot"; 194 type = types.str; 195 description = "Specifies the Buildbot TitleURL."; 196 }; 197 198 dbUrl = mkOption { 199 default = "sqlite:///state.sqlite"; 200 type = types.str; 201 description = "Specifies the database connection string."; 202 }; 203 204 port = mkOption { 205 default = 8010; 206 type = types.int; 207 description = "Specifies port number on which the buildbot HTTP interface listens."; 208 }; 209 210 package = mkOption { 211 type = types.package; 212 default = pkgs.python3Packages.buildbot-full; 213 defaultText = "pkgs.python3Packages.buildbot-full"; 214 description = "Package to use for buildbot."; 215 example = literalExample "pkgs.python3Packages.buildbot"; 216 }; 217 218 packages = mkOption { 219 default = [ pkgs.git ]; 220 example = literalExample "[ pkgs.git ]"; 221 type = types.listOf types.package; 222 description = "Packages to add to PATH for the buildbot process."; 223 }; 224 225 pythonPackages = mkOption { 226 type = types.functionTo (types.listOf types.package); 227 default = pythonPackages: with pythonPackages; [ ]; 228 defaultText = "pythonPackages: with pythonPackages; [ ]"; 229 description = "Packages to add the to the PYTHONPATH of the buildbot process."; 230 example = literalExample "pythonPackages: with pythonPackages; [ requests ]"; 231 }; 232 }; 233 }; 234 235 config = mkIf cfg.enable { 236 users.groups = optionalAttrs (cfg.group == "buildbot") { 237 buildbot = { }; 238 }; 239 240 users.users = optionalAttrs (cfg.user == "buildbot") { 241 buildbot = { 242 description = "Buildbot User."; 243 isNormalUser = true; 244 createHome = true; 245 home = cfg.home; 246 group = cfg.group; 247 extraGroups = cfg.extraGroups; 248 useDefaultShell = true; 249 }; 250 }; 251 252 systemd.services.buildbot-master = { 253 description = "Buildbot Continuous Integration Server."; 254 after = [ "network-online.target" ]; 255 wantedBy = [ "multi-user.target" ]; 256 path = cfg.packages ++ cfg.pythonPackages python.pkgs; 257 environment.PYTHONPATH = "${python.withPackages (self: cfg.pythonPackages self ++ [ cfg.package ])}/${python.sitePackages}"; 258 259 preStart = '' 260 mkdir -vp "${cfg.buildbotDir}" 261 # Link the tac file so buildbot command line tools recognize the directory 262 ln -sf "${tacFile}" "${cfg.buildbotDir}/buildbot.tac" 263 ${cfg.package}/bin/buildbot create-master --db "${cfg.dbUrl}" "${cfg.buildbotDir}" 264 rm -f buildbot.tac.new master.cfg.sample 265 ''; 266 267 serviceConfig = { 268 Type = "simple"; 269 User = cfg.user; 270 Group = cfg.group; 271 WorkingDirectory = cfg.home; 272 # NOTE: call twistd directly with stdout logging for systemd 273 ExecStart = "${python.pkgs.twisted}/bin/twistd -o --nodaemon --pidfile= --logfile - --python ${tacFile}"; 274 }; 275 }; 276 }; 277 278 imports = [ 279 (mkRenamedOptionModule [ "services" "buildbot-master" "bpPort" ] [ "services" "buildbot-master" "pbPort" ]) 280 (mkRemovedOptionModule [ "services" "buildbot-master" "status" ] '' 281 Since Buildbot 0.9.0, status targets are deprecated and ignored. 282 Review your configuration and migrate to reporters (available at services.buildbot-master.reporters). 283 '') 284 ]; 285 286 meta.maintainers = with lib.maintainers; [ mic92 lopsided98 ]; 287}