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