diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2019-05-22 20:55:28 +0200 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2019-05-22 20:55:28 +0200 |
commit | 8d213e2b1c934f6861f76aad5eb7c11097fa97de (patch) | |
tree | 23f8a2d5692deaeffffa1ab5f098b2d24b9e2217 /modules | |
parent | a1a8649a2be768685eb04c246c114fce36b8096f (diff) | |
download | Nix-8d213e2b1c934f6861f76aad5eb7c11097fa97de.tar.gz Nix-8d213e2b1c934f6861f76aad5eb7c11097fa97de.tar.zst Nix-8d213e2b1c934f6861f76aad5eb7c11097fa97de.zip |
Move rest of the modules outside of nixops
Diffstat (limited to 'modules')
23 files changed, 2399 insertions, 1 deletions
diff --git a/modules/private/buildbot/common/build_helpers.py b/modules/private/buildbot/common/build_helpers.py new file mode 100644 index 0000000..384b1ac --- /dev/null +++ b/modules/private/buildbot/common/build_helpers.py | |||
@@ -0,0 +1,256 @@ | |||
1 | from buildbot.plugins import util, steps, schedulers | ||
2 | from buildbot_buildslist import BuildsList | ||
3 | |||
4 | __all__ = [ | ||
5 | "force_scheduler", "deploy_scheduler", "hook_scheduler", | ||
6 | "clean_branch", "package_and_upload", "SlackStatusPush", | ||
7 | "XMPPStatusPush" | ||
8 | ] | ||
9 | |||
10 | # Small helpers" | ||
11 | @util.renderer | ||
12 | def clean_branch(props): | ||
13 | if props.hasProperty("branch") and len(props["branch"]) > 0: | ||
14 | return props["branch"].replace("/", "_") | ||
15 | else: | ||
16 | return "HEAD" | ||
17 | |||
18 | def package_and_upload(package, package_dest, package_url): | ||
19 | return [ | ||
20 | steps.ShellCommand(name="build package", | ||
21 | logEnviron=False, haltOnFailure=True, workdir="source", | ||
22 | command=["git", "archive", "HEAD", "-o", package]), | ||
23 | |||
24 | steps.FileUpload(name="upload package", workersrc=package, | ||
25 | workdir="source", masterdest=package_dest, | ||
26 | url=package_url, mode=0o644), | ||
27 | |||
28 | steps.ShellCommand(name="cleanup package", logEnviron=False, | ||
29 | haltOnFailure=True, workdir="source", alwaysRun=True, | ||
30 | command=["rm", "-f", package]), | ||
31 | ] | ||
32 | |||
33 | # Schedulers | ||
34 | def force_scheduler(name, builders): | ||
35 | return schedulers.ForceScheduler(name=name, | ||
36 | label="Force build", buttonName="Force build", | ||
37 | reason=util.StringParameter(name="reason", label="Reason", default="Force build"), | ||
38 | codebases=[ | ||
39 | util.CodebaseParameter("", | ||
40 | branch=util.StringParameter( | ||
41 | name="branch", label="Git reference (tag, branch)", required=True), | ||
42 | revision=util.FixedParameter(name="revision", default=""), | ||
43 | repository=util.FixedParameter(name="repository", default=""), | ||
44 | project=util.FixedParameter(name="project", default=""), | ||
45 | ), | ||
46 | ], | ||
47 | username=util.FixedParameter(name="username", default="Web button"), | ||
48 | builderNames=builders) | ||
49 | |||
50 | def deploy_scheduler(name, builders): | ||
51 | return schedulers.ForceScheduler(name=name, | ||
52 | builderNames=builders, | ||
53 | label="Deploy built package", buttonName="Deploy", | ||
54 | username=util.FixedParameter(name="username", default="Web button"), | ||
55 | codebases=[ | ||
56 | util.CodebaseParameter(codebase="", | ||
57 | branch=util.FixedParameter(name="branch", default=""), | ||
58 | revision=util.FixedParameter(name="revision", default=""), | ||
59 | repository=util.FixedParameter(name="repository", default=""), | ||
60 | project=util.FixedParameter(name="project", default=""))], | ||
61 | reason=util.FixedParameter(name="reason", default="Deploy"), | ||
62 | properties=[ | ||
63 | util.ChoiceStringParameter(label="Environment", | ||
64 | name="environment", default="integration", | ||
65 | choices=["integration", "production"]), | ||
66 | BuildsList(label="Build to deploy", name="build"), | ||
67 | ] | ||
68 | ) | ||
69 | |||
70 | def hook_scheduler(project, timer=10): | ||
71 | return schedulers.AnyBranchScheduler( | ||
72 | change_filter=util.ChangeFilter(category="hooks", project=project), | ||
73 | name=project, treeStableTimer=timer, builderNames=["{}_build".format(project)]) | ||
74 | |||
75 | # Slack/XMPP status push | ||
76 | from buildbot.reporters.http import HttpStatusPushBase | ||
77 | from twisted.internet import defer | ||
78 | from twisted.python import log | ||
79 | from buildbot.util import httpclientservice | ||
80 | from buildbot.reporters import utils | ||
81 | from buildbot.process import results | ||
82 | from twisted.words.protocols.jabber.jid import JID | ||
83 | from wokkel import client, xmppim | ||
84 | from functools import partial | ||
85 | |||
86 | class SlackStatusPush(HttpStatusPushBase): | ||
87 | name = "SlackStatusPush" | ||
88 | |||
89 | @defer.inlineCallbacks | ||
90 | def reconfigService(self, serverUrl, **kwargs): | ||
91 | yield HttpStatusPushBase.reconfigService(self, **kwargs) | ||
92 | self._http = yield httpclientservice.HTTPClientService.getService( | ||
93 | self.master, serverUrl) | ||
94 | |||
95 | @defer.inlineCallbacks | ||
96 | def send(self, build): | ||
97 | yield utils.getDetailsForBuild(self.master, build, wantProperties=True) | ||
98 | response = yield self._http.post("", json=self.format(build)) | ||
99 | if response.code != 200: | ||
100 | log.msg("%s: unable to upload status: %s" % | ||
101 | (response.code, response.content)) | ||
102 | |||
103 | def format(self, build): | ||
104 | colors = [ | ||
105 | "#36A64F", # success | ||
106 | "#F1E903", # warnings | ||
107 | "#DA0505", # failure | ||
108 | "#FFFFFF", # skipped | ||
109 | "#000000", # exception | ||
110 | "#FFFFFF", # retry | ||
111 | "#D02CA9", # cancelled | ||
112 | ] | ||
113 | |||
114 | if "environment" in build["properties"]: | ||
115 | msg = "{} environment".format(build["properties"]["environment"][0]) | ||
116 | if "build" in build["properties"]: | ||
117 | msg = "of archive {} in ".format(build["properties"]["build"][0]) + msg | ||
118 | elif len(build["buildset"]["sourcestamps"][0]["branch"]) > 0: | ||
119 | msg = "revision {}".format(build["buildset"]["sourcestamps"][0]["branch"]) | ||
120 | else: | ||
121 | msg = "build" | ||
122 | |||
123 | if build["complete"]: | ||
124 | timedelta = int((build["complete_at"] - build["started_at"]).total_seconds()) | ||
125 | hours, rest = divmod(timedelta, 3600) | ||
126 | minutes, seconds = divmod(rest, 60) | ||
127 | if hours > 0: | ||
128 | duration = "{}h {}min {}s".format(hours, minutes, seconds) | ||
129 | elif minutes > 0: | ||
130 | duration = "{}min {}s".format(minutes, seconds) | ||
131 | else: | ||
132 | duration = "{}s".format(seconds) | ||
133 | |||
134 | text = "Build <{}|{}> of {}'s {} was {} in {}.".format( | ||
135 | build["url"], build["buildid"], | ||
136 | build["builder"]["name"], | ||
137 | msg, | ||
138 | results.Results[build["results"]], | ||
139 | duration, | ||
140 | ) | ||
141 | fields = [ | ||
142 | { | ||
143 | "title": "Build", | ||
144 | "value": "<{}|{}>".format(build["url"], build["buildid"]), | ||
145 | "short": True, | ||
146 | }, | ||
147 | { | ||
148 | "title": "Project", | ||
149 | "value": build["builder"]["name"], | ||
150 | "short": True, | ||
151 | }, | ||
152 | { | ||
153 | "title": "Build status", | ||
154 | "value": results.Results[build["results"]], | ||
155 | "short": True, | ||
156 | }, | ||
157 | { | ||
158 | "title": "Build duration", | ||
159 | "value": duration, | ||
160 | "short": True, | ||
161 | }, | ||
162 | ] | ||
163 | if "environment" in build["properties"]: | ||
164 | fields.append({ | ||
165 | "title": "Environment", | ||
166 | "value": build["properties"]["environment"][0], | ||
167 | "short": True, | ||
168 | }) | ||
169 | if "build" in build["properties"]: | ||
170 | fields.append({ | ||
171 | "title": "Archive", | ||
172 | "value": build["properties"]["build"][0], | ||
173 | "short": True, | ||
174 | }) | ||
175 | attachments = [{ | ||
176 | "fallback": "", | ||
177 | "color": colors[build["results"]], | ||
178 | "fields": fields | ||
179 | }] | ||
180 | else: | ||
181 | text = "Build <{}|{}> of {}'s {} started.".format( | ||
182 | build["url"], build["buildid"], | ||
183 | build["builder"]["name"], | ||
184 | msg, | ||
185 | ) | ||
186 | attachments = [] | ||
187 | |||
188 | return { | ||
189 | "username": "Buildbot", | ||
190 | "icon_url": "http://docs.buildbot.net/current/_static/icon.png", | ||
191 | "text": text, | ||
192 | "attachments": attachments, | ||
193 | } | ||
194 | |||
195 | class XMPPStatusPush(HttpStatusPushBase): | ||
196 | name = "XMPPStatusPush" | ||
197 | |||
198 | @defer.inlineCallbacks | ||
199 | def reconfigService(self, password, recipients, **kwargs): | ||
200 | yield HttpStatusPushBase.reconfigService(self, **kwargs) | ||
201 | self.password = password | ||
202 | self.recipients = recipients | ||
203 | |||
204 | @defer.inlineCallbacks | ||
205 | def send(self, build): | ||
206 | yield utils.getDetailsForBuild(self.master, build, wantProperties=True) | ||
207 | body = self.format(build) | ||
208 | factory = client.DeferredClientFactory(JID("notify_bot@immae.fr/buildbot"), self.password) | ||
209 | d = client.clientCreator(factory) | ||
210 | def send_message(recipient, stream): | ||
211 | message = xmppim.Message(recipient=JID(recipient), body=body) | ||
212 | message.stanzaType = 'chat' | ||
213 | stream.send(message.toElement()) | ||
214 | # To allow chaining | ||
215 | return stream | ||
216 | for recipient in self.recipients: | ||
217 | d.addCallback(partial(send_message, recipient)) | ||
218 | d.addCallback(lambda _: factory.streamManager.xmlstream.sendFooter()) | ||
219 | d.addErrback(log.err) | ||
220 | |||
221 | def format(self, build): | ||
222 | if "environment" in build["properties"]: | ||
223 | msg = "{} environment".format(build["properties"]["environment"][0]) | ||
224 | if "build" in build["properties"]: | ||
225 | msg = "of archive {} in ".format(build["properties"]["build"][0]) + msg | ||
226 | elif len(build["buildset"]["sourcestamps"][0]["branch"]) > 0: | ||
227 | msg = "revision {}".format(build["buildset"]["sourcestamps"][0]["branch"]) | ||
228 | else: | ||
229 | msg = "build" | ||
230 | |||
231 | if build["complete"]: | ||
232 | timedelta = int((build["complete_at"] - build["started_at"]).total_seconds()) | ||
233 | hours, rest = divmod(timedelta, 3600) | ||
234 | minutes, seconds = divmod(rest, 60) | ||
235 | if hours > 0: | ||
236 | duration = "{}h {}min {}s".format(hours, minutes, seconds) | ||
237 | elif minutes > 0: | ||
238 | duration = "{}min {}s".format(minutes, seconds) | ||
239 | else: | ||
240 | duration = "{}s".format(seconds) | ||
241 | |||
242 | text = "Build {} ( {} ) of {}'s {} was {} in {}.".format( | ||
243 | build["buildid"], build["url"], | ||
244 | build["builder"]["name"], | ||
245 | msg, | ||
246 | results.Results[build["results"]], | ||
247 | duration, | ||
248 | ) | ||
249 | else: | ||
250 | text = "Build {} ( {} ) of {}'s {} started.".format( | ||
251 | build["buildid"], build["url"], | ||
252 | build["builder"]["name"], | ||
253 | msg, | ||
254 | ) | ||
255 | |||
256 | return text | ||
diff --git a/modules/private/buildbot/common/master.cfg b/modules/private/buildbot/common/master.cfg new file mode 100644 index 0000000..abe08e0 --- /dev/null +++ b/modules/private/buildbot/common/master.cfg | |||
@@ -0,0 +1,69 @@ | |||
1 | # -*- python -*- | ||
2 | # ex: set filetype=python: | ||
3 | |||
4 | from buildbot.plugins import secrets, util, webhooks | ||
5 | from buildbot.util import bytes2unicode | ||
6 | import re | ||
7 | import os | ||
8 | from buildbot_config import E, configure | ||
9 | import json | ||
10 | |||
11 | class CustomBase(webhooks.base): | ||
12 | def getChanges(self, request): | ||
13 | try: | ||
14 | content = request.content.read() | ||
15 | args = json.loads(bytes2unicode(content)) | ||
16 | except Exception as e: | ||
17 | raise ValueError("Error loading JSON: " + str(e)) | ||
18 | |||
19 | args.setdefault("comments", "") | ||
20 | args.setdefault("repository", "") | ||
21 | args.setdefault("author", args.get("who")) | ||
22 | |||
23 | return ([args], None) | ||
24 | |||
25 | userInfoProvider = util.LdapUserInfo( | ||
26 | uri=E.LDAP_URL, | ||
27 | bindUser=E.LDAP_ADMIN_USER, | ||
28 | bindPw=open(E.SECRETS_FILE + "/ldap", "r").read().rstrip(), | ||
29 | accountBase=E.LDAP_BASE, | ||
30 | accountPattern=E.LDAP_PATTERN, | ||
31 | accountFullName='cn', | ||
32 | accountEmail='mail', | ||
33 | avatarData="jpegPhoto", | ||
34 | groupBase=E.LDAP_BASE, | ||
35 | groupName="cn", | ||
36 | groupMemberPattern=E.LDAP_GROUP_PATTERN, | ||
37 | ) | ||
38 | |||
39 | c = BuildmasterConfig = { | ||
40 | "title": E.TITLE, | ||
41 | "titleURL": E.TITLE_URL, | ||
42 | "db": { | ||
43 | "db_url": "sqlite:///state.sqlite" | ||
44 | }, | ||
45 | "protocols": { "pb": { "port": E.PB_SOCKET } }, | ||
46 | "workers": [], | ||
47 | "change_source": [], | ||
48 | "schedulers": [], | ||
49 | "builders": [], | ||
50 | "services": [], | ||
51 | "secretsProviders": [ | ||
52 | secrets.SecretInAFile(E.SECRETS_FILE), | ||
53 | ], | ||
54 | "www": { | ||
55 | "change_hook_dialects": { "base": { "custom_class": CustomBase } }, | ||
56 | "plugins": { | ||
57 | "waterfall_view": {}, | ||
58 | "console_view": {}, | ||
59 | "grid_view": {}, | ||
60 | "buildslist": {}, | ||
61 | }, | ||
62 | "auth": util.RemoteUserAuth( | ||
63 | header=b"X-Remote-User", | ||
64 | userInfoProvider=userInfoProvider, | ||
65 | headerRegex=re.compile(br"(?P<username>[^ @]+)")), | ||
66 | } | ||
67 | } | ||
68 | |||
69 | configure(c) | ||
diff --git a/modules/private/buildbot/default.nix b/modules/private/buildbot/default.nix new file mode 100644 index 0000000..fa6a6f2 --- /dev/null +++ b/modules/private/buildbot/default.nix | |||
@@ -0,0 +1,198 @@ | |||
1 | { lib, pkgs, config, myconfig, ... }: | ||
2 | let | ||
3 | varDir = "/var/lib/buildbot"; | ||
4 | buildbot_common = pkgs.python3Packages.buildPythonPackage rec { | ||
5 | name = "buildbot_common"; | ||
6 | src = ./common; | ||
7 | format = "other"; | ||
8 | installPhase = '' | ||
9 | mkdir -p $out/${pkgs.python3.pythonForBuild.sitePackages} | ||
10 | cp -a $src $out/${pkgs.python3.pythonForBuild.sitePackages}/buildbot_common | ||
11 | ''; | ||
12 | }; | ||
13 | buildbot = pkgs.python3Packages.buildbot-full; | ||
14 | in | ||
15 | { | ||
16 | options = { | ||
17 | myServices.buildbot.enable = lib.mkOption { | ||
18 | type = lib.types.bool; | ||
19 | default = false; | ||
20 | description = '' | ||
21 | Whether to enable buildbot. | ||
22 | ''; | ||
23 | }; | ||
24 | }; | ||
25 | |||
26 | config = lib.mkIf config.myServices.buildbot.enable { | ||
27 | ids.uids.buildbot = myconfig.env.buildbot.user.uid; | ||
28 | ids.gids.buildbot = myconfig.env.buildbot.user.gid; | ||
29 | |||
30 | users.groups.buildbot.gid = config.ids.gids.buildbot; | ||
31 | users.users.buildbot = { | ||
32 | name = "buildbot"; | ||
33 | uid = config.ids.uids.buildbot; | ||
34 | group = "buildbot"; | ||
35 | description = "Buildbot user"; | ||
36 | home = varDir; | ||
37 | extraGroups = [ "keys" ]; | ||
38 | }; | ||
39 | |||
40 | services.websites.tools.vhostConfs.git.extraConfig = lib.attrsets.mapAttrsToList (k: project: '' | ||
41 | RedirectMatch permanent "^/buildbot/${project.name}$" "/buildbot/${project.name}/" | ||
42 | RewriteEngine On | ||
43 | RewriteRule ^/buildbot/${project.name}/ws(.*)$ unix:///run/buildbot/${project.name}.sock|ws://git.immae.eu/ws$1 [P,NE,QSA,L] | ||
44 | ProxyPass /buildbot/${project.name}/ unix:///run/buildbot/${project.name}.sock|http://${project.name}-git.immae.eu/ | ||
45 | ProxyPassReverse /buildbot/${project.name}/ unix:///run/buildbot/${project.name}.sock|http://${project.name}-git.immae.eu/ | ||
46 | <Location /buildbot/${project.name}/> | ||
47 | Use LDAPConnect | ||
48 | Require ldap-group cn=users,ou=${project.name},cn=buildbot,ou=services,dc=immae,dc=eu | ||
49 | |||
50 | SetEnvIf X-Url-Scheme https HTTPS=1 | ||
51 | ProxyPreserveHost On | ||
52 | </Location> | ||
53 | <Location /buildbot/${project.name}/change_hook/base> | ||
54 | <RequireAny> | ||
55 | Require local | ||
56 | Require ldap-group cn=users,ou=${project.name},cn=buildbot,ou=services,dc=immae,dc=eu | ||
57 | Include /var/secrets/buildbot/${project.name}/webhook-httpd-include | ||
58 | </RequireAny> | ||
59 | </Location> | ||
60 | '') myconfig.env.buildbot.projects; | ||
61 | |||
62 | system.activationScripts = lib.attrsets.mapAttrs' (k: project: lib.attrsets.nameValuePair "buildbot-${project.name}" { | ||
63 | deps = [ "users" "wrappers" ]; | ||
64 | text = project.activationScript; | ||
65 | }) myconfig.env.buildbot.projects; | ||
66 | |||
67 | secrets.keys = ( | ||
68 | lib.lists.flatten ( | ||
69 | lib.attrsets.mapAttrsToList (k: project: | ||
70 | lib.attrsets.mapAttrsToList (k: v: | ||
71 | { | ||
72 | permissions = "0600"; | ||
73 | user = "buildbot"; | ||
74 | group = "buildbot"; | ||
75 | text = v; | ||
76 | dest = "buildbot/${project.name}/${k}"; | ||
77 | } | ||
78 | ) project.secrets | ||
79 | ++ [ | ||
80 | { | ||
81 | permissions = "0600"; | ||
82 | user = "wwwrun"; | ||
83 | group = "wwwrun"; | ||
84 | text = lib.optionalString (lib.attrsets.hasAttr "webhookTokens" project) '' | ||
85 | Require expr "req('Access-Key') in { ${builtins.concatStringsSep ", " (map (x: "'${x}'") project.webhookTokens)} }" | ||
86 | ''; | ||
87 | dest = "buildbot/${project.name}/webhook-httpd-include"; | ||
88 | } | ||
89 | ] | ||
90 | ) myconfig.env.buildbot.projects | ||
91 | ) | ||
92 | ) ++ [ | ||
93 | { | ||
94 | permissions = "0600"; | ||
95 | user = "buildbot"; | ||
96 | group = "buildbot"; | ||
97 | text = myconfig.env.buildbot.ldap.password; | ||
98 | dest = "buildbot/ldap"; | ||
99 | } | ||
100 | { | ||
101 | permissions = "0600"; | ||
102 | user = "buildbot"; | ||
103 | group = "buildbot"; | ||
104 | text = builtins.readFile "${myconfig.privateFiles}/buildbot_ssh_key"; | ||
105 | dest = "buildbot/ssh_key"; | ||
106 | } | ||
107 | ]; | ||
108 | |||
109 | systemd.services = lib.attrsets.mapAttrs' (k: project: lib.attrsets.nameValuePair "buildbot-${project.name}" { | ||
110 | description = "Buildbot Continuous Integration Server ${project.name}."; | ||
111 | after = [ "network-online.target" ]; | ||
112 | wantedBy = [ "multi-user.target" ]; | ||
113 | path = project.packages pkgs ++ (project.pythonPackages buildbot.pythonModule pkgs); | ||
114 | preStart = let | ||
115 | master-cfg = "${buildbot_common}/${pkgs.python3.pythonForBuild.sitePackages}/buildbot_common/master.cfg"; | ||
116 | tac_file = pkgs.writeText "buildbot.tac" '' | ||
117 | import os | ||
118 | |||
119 | from twisted.application import service | ||
120 | from buildbot.master import BuildMaster | ||
121 | |||
122 | basedir = '${varDir}/${project.name}' | ||
123 | rotateLength = 10000000 | ||
124 | maxRotatedFiles = 10 | ||
125 | configfile = '${master-cfg}' | ||
126 | |||
127 | # Default umask for server | ||
128 | umask = None | ||
129 | |||
130 | # if this is a relocatable tac file, get the directory containing the TAC | ||
131 | if basedir == '.': | ||
132 | import os | ||
133 | basedir = os.path.abspath(os.path.dirname(__file__)) | ||
134 | |||
135 | # note: this line is matched against to check that this is a buildmaster | ||
136 | # directory; do not edit it. | ||
137 | application = service.Application('buildmaster') | ||
138 | from twisted.python.logfile import LogFile | ||
139 | from twisted.python.log import ILogObserver, FileLogObserver | ||
140 | logfile = LogFile.fromFullPath(os.path.join(basedir, "twistd.log"), rotateLength=rotateLength, | ||
141 | maxRotatedFiles=maxRotatedFiles) | ||
142 | application.setComponent(ILogObserver, FileLogObserver(logfile).emit) | ||
143 | |||
144 | m = BuildMaster(basedir, configfile, umask) | ||
145 | m.setServiceParent(application) | ||
146 | m.log_rotation.rotateLength = rotateLength | ||
147 | m.log_rotation.maxRotatedFiles = maxRotatedFiles | ||
148 | ''; | ||
149 | in '' | ||
150 | if [ ! -f ${varDir}/${project.name}/buildbot.tac ]; then | ||
151 | ${buildbot}/bin/buildbot create-master -c "${master-cfg}" "${varDir}/${project.name}" | ||
152 | rm -f ${varDir}/${project.name}/master.cfg.sample | ||
153 | rm -f ${varDir}/${project.name}/buildbot.tac | ||
154 | fi | ||
155 | ln -sf ${tac_file} ${varDir}/${project.name}/buildbot.tac | ||
156 | # different buildbots may be trying that simultaneously, add the || true to avoid complaining in case of race | ||
157 | install -Dm600 -o buildbot -g buildbot -T /var/secrets/buildbot/ssh_key ${varDir}/buildbot_key || true | ||
158 | buildbot_secrets=${varDir}/${project.name}/secrets | ||
159 | install -m 0700 -o buildbot -g buildbot -d $buildbot_secrets | ||
160 | install -Dm600 -o buildbot -g buildbot -T /var/secrets/buildbot/ldap $buildbot_secrets/ldap | ||
161 | ${builtins.concatStringsSep "\n" (lib.attrsets.mapAttrsToList | ||
162 | (k: v: "install -Dm600 -o buildbot -g buildbot -T /var/secrets/buildbot/${project.name}/${k} $buildbot_secrets/${k}") project.secrets | ||
163 | )} | ||
164 | ''; | ||
165 | environment = let | ||
166 | project_env = lib.attrsets.mapAttrs' (k: v: lib.attrsets.nameValuePair "BUILDBOT_${k}" v) project.environment; | ||
167 | buildbot_config = pkgs.python3Packages.buildPythonPackage (rec { | ||
168 | name = "buildbot_config-${project.name}"; | ||
169 | src = ./projects + "/${project.name}"; | ||
170 | format = "other"; | ||
171 | installPhase = '' | ||
172 | mkdir -p $out/${pkgs.python3.pythonForBuild.sitePackages} | ||
173 | cp -a $src $out/${pkgs.python3.pythonForBuild.sitePackages}/buildbot_config | ||
174 | ''; | ||
175 | }); | ||
176 | HOME = "${varDir}/${project.name}"; | ||
177 | PYTHONPATH = "${buildbot.pythonModule.withPackages (self: project.pythonPackages self pkgs ++ [ | ||
178 | pkgs.python3Packages.wokkel | ||
179 | pkgs.python3Packages.treq pkgs.python3Packages.ldap3 buildbot | ||
180 | pkgs.python3Packages.buildbot-worker | ||
181 | buildbot_common buildbot_config | ||
182 | ])}/${buildbot.pythonModule.sitePackages}${if project.pythonPathHome then ":${varDir}/${project.name}/.local/${pkgs.python3.pythonForBuild.sitePackages}" else ""}"; | ||
183 | in project_env // { inherit PYTHONPATH HOME; }; | ||
184 | |||
185 | serviceConfig = { | ||
186 | Type = "forking"; | ||
187 | User = "buildbot"; | ||
188 | Group = "buildbot"; | ||
189 | RuntimeDirectory = "buildbot"; | ||
190 | RuntimeDirectoryPreserve = "yes"; | ||
191 | StateDirectory = "buildbot"; | ||
192 | SupplementaryGroups = "keys"; | ||
193 | WorkingDirectory = "${varDir}/${project.name}"; | ||
194 | ExecStart = "${buildbot}/bin/buildbot start"; | ||
195 | }; | ||
196 | }) myconfig.env.buildbot.projects; | ||
197 | }; | ||
198 | } | ||
diff --git a/modules/private/buildbot/projects/caldance/__init__.py b/modules/private/buildbot/projects/caldance/__init__.py new file mode 100644 index 0000000..2c0bad5 --- /dev/null +++ b/modules/private/buildbot/projects/caldance/__init__.py | |||
@@ -0,0 +1,190 @@ | |||
1 | from buildbot.plugins import * | ||
2 | from buildbot_common.build_helpers import * | ||
3 | import os | ||
4 | from buildbot.util import bytes2unicode | ||
5 | import json | ||
6 | |||
7 | __all__ = [ "configure", "E" ] | ||
8 | |||
9 | class E(): | ||
10 | PROJECT = "caldance" | ||
11 | BUILDBOT_URL = "https://git.immae.eu/buildbot/{}/".format(PROJECT) | ||
12 | SOCKET = "unix:/run/buildbot/{}.sock".format(PROJECT) | ||
13 | PB_SOCKET = "unix:address=/run/buildbot/{}_pb.sock".format(PROJECT) | ||
14 | RELEASE_PATH = "/var/lib/ftp/release.immae.eu/{}".format(PROJECT) | ||
15 | RELEASE_URL = "https://release.immae.eu/{}".format(PROJECT) | ||
16 | GIT_URL = "gitolite@git.immae.eu:perso/simon_descarpentries/www.cal-dance.com" | ||
17 | SSH_KEY_PATH = "/var/lib/buildbot/buildbot_key" | ||
18 | SSH_HOST_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIFbhFTl2A2RJn5L51yxJM4XfCS2ZaiSX/jo9jFSdghF" | ||
19 | LDAP_HOST = "ldap.immae.eu" | ||
20 | LDAP_DN = "cn=buildbot,ou=services,dc=immae,dc=eu" | ||
21 | LDAP_ROLES_BASE = "ou=roles,ou=hosts,dc=immae,dc=eu" | ||
22 | XMPP_RECIPIENTS = os.environ["BUILDBOT_XMPP_RECIPIENTS"].split(" ") | ||
23 | |||
24 | PUPPET_HOST = { | ||
25 | "integration": "root@caldance.immae.eu", | ||
26 | } | ||
27 | |||
28 | # master.cfg | ||
29 | SECRETS_FILE = os.getcwd() + "/secrets" | ||
30 | LDAP_URL = "ldaps://ldap.immae.eu:636" | ||
31 | LDAP_ADMIN_USER = "cn=buildbot,ou=services,dc=immae,dc=eu" | ||
32 | LDAP_BASE = "dc=immae,dc=eu" | ||
33 | LDAP_PATTERN = "(uid=%(username)s)" | ||
34 | LDAP_GROUP_PATTERN = "(&(memberOf=cn=groups,ou=caldance,cn=buildbot,ou=services,dc=immae,dc=eu)(member=%(dn)s))" | ||
35 | TITLE_URL = "https://caldance.immae.eu" | ||
36 | TITLE = "Caldance" | ||
37 | |||
38 | class CustomBase(webhooks.base): | ||
39 | def getChanges(self, request): | ||
40 | try: | ||
41 | content = request.content.read() | ||
42 | args = json.loads(bytes2unicode(content)) | ||
43 | except Exception as e: | ||
44 | raise ValueError("Error loading JSON: " + str(e)) | ||
45 | |||
46 | args.setdefault("comments", "") | ||
47 | args.setdefault("repository", "") | ||
48 | args.setdefault("author", args.get("who", "unknown")) | ||
49 | |||
50 | if args["category"] == "deploy_webhook": | ||
51 | args = { | ||
52 | "category": "deploy_webhook", | ||
53 | "comments": "", | ||
54 | "repository": "", | ||
55 | "author": "webhook", | ||
56 | "project": "Caldance", | ||
57 | "properties": { | ||
58 | "environment": args.get("environment", "integration"), | ||
59 | "build": "caldance_{}.tar.gz".format(args.get("build", "master")) | ||
60 | } | ||
61 | } | ||
62 | |||
63 | return ([args], None) | ||
64 | |||
65 | def deploy_hook_scheduler(project, timer=1): | ||
66 | return schedulers.AnyBranchScheduler( | ||
67 | change_filter=util.ChangeFilter(category="deploy_webhook", project=project), | ||
68 | name="{}_deploy".format(project), treeStableTimer=timer, builderNames=["{}_deploy".format(project)]) | ||
69 | |||
70 | def configure(c): | ||
71 | c["buildbotURL"] = E.BUILDBOT_URL | ||
72 | c["www"]["port"] = E.SOCKET | ||
73 | |||
74 | c["www"]["change_hook_dialects"]["base"] = { | ||
75 | "custom_class": CustomBase | ||
76 | } | ||
77 | c['workers'].append(worker.LocalWorker("generic-worker")) | ||
78 | c['workers'].append(worker.LocalWorker("deploy-worker")) | ||
79 | |||
80 | c['schedulers'].append(hook_scheduler("Caldance", timer=1)) | ||
81 | c['schedulers'].append(force_scheduler("force_caldance", ["Caldance_build"])) | ||
82 | c['schedulers'].append(deploy_scheduler("deploy_caldance", ["Caldance_deploy"])) | ||
83 | c['schedulers'].append(deploy_hook_scheduler("Caldance", timer=1)) | ||
84 | |||
85 | c['builders'].append(factory("caldance")) | ||
86 | |||
87 | c['builders'].append(deploy_factory("caldance")) | ||
88 | |||
89 | c['services'].append(SlackStatusPush( | ||
90 | name="slack_status_caldance", | ||
91 | builders=["Caldance_build", "Caldance_deploy"], | ||
92 | serverUrl=open(E.SECRETS_FILE + "/slack_webhook", "r").read().rstrip())) | ||
93 | c['services'].append(XMPPStatusPush( | ||
94 | name="xmpp_status_caldance", | ||
95 | builders=["Caldance_build", "Caldance_deploy"], | ||
96 | recipients=E.XMPP_RECIPIENTS, | ||
97 | password=open(E.SECRETS_FILE + "/notify_xmpp_password", "r").read().rstrip())) | ||
98 | |||
99 | def factory(project, ignore_fails=False): | ||
100 | release_file = "{1}/{0}_%(kw:clean_branch)s.tar.gz" | ||
101 | |||
102 | package = util.Interpolate("{0}_%(kw:clean_branch)s.tar.gz".format(project), clean_branch=clean_branch) | ||
103 | package_dest = util.Interpolate(release_file.format(project, E.RELEASE_PATH), clean_branch=clean_branch) | ||
104 | package_url = util.Interpolate(release_file.format(project, E.RELEASE_URL), clean_branch=clean_branch) | ||
105 | |||
106 | factory = util.BuildFactory() | ||
107 | factory.addStep(steps.Git(logEnviron=False, repourl=E.GIT_URL, | ||
108 | sshPrivateKey=open(E.SSH_KEY_PATH).read().rstrip(), | ||
109 | sshHostKey=E.SSH_HOST_KEY, mode="full", method="copy")) | ||
110 | factory.addSteps(package_and_upload(package, package_dest, package_url)) | ||
111 | |||
112 | return util.BuilderConfig( | ||
113 | name="{}_build".format(project.capitalize()), | ||
114 | workernames=["generic-worker"], factory=factory) | ||
115 | |||
116 | def compute_build_infos(project): | ||
117 | @util.renderer | ||
118 | def compute(props): | ||
119 | import re, hashlib | ||
120 | build_file = props.getProperty("build") | ||
121 | package_dest = "{1}/{0}".format(build_file, E.RELEASE_PATH) | ||
122 | version = re.match(r"{0}_(.*).tar.gz".format(project), build_file).group(1) | ||
123 | with open(package_dest, "rb") as f: | ||
124 | sha = hashlib.sha256(f.read()).hexdigest() | ||
125 | return { | ||
126 | "build_version": version, | ||
127 | "build_hash": sha, | ||
128 | } | ||
129 | return compute | ||
130 | |||
131 | @util.renderer | ||
132 | def puppet_host(props): | ||
133 | environment = props["environment"] if props.hasProperty("environment") else "integration" | ||
134 | return E.PUPPET_HOST.get(environment, "host.invalid") | ||
135 | |||
136 | def deploy_factory(project): | ||
137 | package_dest = util.Interpolate("{0}/%(prop:build)s".format(E.RELEASE_PATH)) | ||
138 | |||
139 | factory = util.BuildFactory() | ||
140 | factory.addStep(steps.MasterShellCommand(command=["test", "-f", package_dest])) | ||
141 | factory.addStep(steps.SetProperties(properties=compute_build_infos(project))) | ||
142 | factory.addStep(LdapPush(environment=util.Property("environment"), | ||
143 | project=project, build_version=util.Property("build_version"), | ||
144 | build_hash=util.Property("build_hash"), ldap_password=util.Secret("ldap"))) | ||
145 | factory.addStep(steps.MasterShellCommand(command=[ | ||
146 | "ssh", "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "CheckHostIP=no", "-i", E.SSH_KEY_PATH, puppet_host])) | ||
147 | return util.BuilderConfig(name="{}_deploy".format(project.capitalize()), workernames=["deploy-worker"], factory=factory) | ||
148 | |||
149 | from twisted.internet import defer | ||
150 | from buildbot.process.buildstep import FAILURE | ||
151 | from buildbot.process.buildstep import SUCCESS | ||
152 | from buildbot.process.buildstep import BuildStep | ||
153 | |||
154 | class LdapPush(BuildStep): | ||
155 | name = "LdapPush" | ||
156 | renderables = ["environment", "project", "build_version", "build_hash", "ldap_password"] | ||
157 | |||
158 | def __init__(self, **kwargs): | ||
159 | self.environment = kwargs.pop("environment") | ||
160 | self.project = kwargs.pop("project") | ||
161 | self.build_version = kwargs.pop("build_version") | ||
162 | self.build_hash = kwargs.pop("build_hash") | ||
163 | self.ldap_password = kwargs.pop("ldap_password") | ||
164 | self.ldap_host = kwargs.pop("ldap_host", E.LDAP_HOST) | ||
165 | super().__init__(**kwargs) | ||
166 | |||
167 | def run(self): | ||
168 | import json | ||
169 | from ldap3 import Reader, Writer, Server, Connection, ObjectDef | ||
170 | server = Server(self.ldap_host) | ||
171 | conn = Connection(server, | ||
172 | user=E.LDAP_DN, | ||
173 | password=self.ldap_password) | ||
174 | conn.bind() | ||
175 | obj = ObjectDef("immaePuppetClass", conn) | ||
176 | r = Reader(conn, obj, | ||
177 | "cn=caldance.{},{}".format(self.environment, E.LDAP_ROLES_BASE)) | ||
178 | r.search() | ||
179 | if len(r) > 0: | ||
180 | w = Writer.from_cursor(r) | ||
181 | for value in w[0].immaePuppetJson.values: | ||
182 | config = json.loads(value) | ||
183 | if "role::caldance::{}_version".format(self.project) in config: | ||
184 | config["role::caldance::{}_version".format(self.project)] = self.build_version | ||
185 | config["role::caldance::{}_sha256".format(self.project)] = self.build_hash | ||
186 | w[0].immaePuppetJson -= value | ||
187 | w[0].immaePuppetJson += json.dumps(config, indent=" ") | ||
188 | w.commit() | ||
189 | return defer.succeed(SUCCESS) | ||
190 | return defer.succeed(FAILURE) | ||
diff --git a/modules/private/buildbot/projects/cryptoportfolio/__init__.py b/modules/private/buildbot/projects/cryptoportfolio/__init__.py new file mode 100644 index 0000000..5d70f95 --- /dev/null +++ b/modules/private/buildbot/projects/cryptoportfolio/__init__.py | |||
@@ -0,0 +1,169 @@ | |||
1 | from buildbot.plugins import * | ||
2 | from buildbot_common.build_helpers import * | ||
3 | import os | ||
4 | |||
5 | __all__ = [ "configure", "E" ] | ||
6 | |||
7 | class E(): | ||
8 | PROJECT = "cryptoportfolio" | ||
9 | BUILDBOT_URL = "https://git.immae.eu/buildbot/{}/".format(PROJECT) | ||
10 | SOCKET = "unix:/run/buildbot/{}.sock".format(PROJECT) | ||
11 | PB_SOCKET = "unix:address=/run/buildbot/{}_pb.sock".format(PROJECT) | ||
12 | RELEASE_PATH = "/var/lib/ftp/release.immae.eu/{}".format(PROJECT) | ||
13 | RELEASE_URL = "https://release.immae.eu/{}".format(PROJECT) | ||
14 | GIT_URL = "https://git.immae.eu/perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/{0}.git" | ||
15 | SSH_KEY_PATH = "/var/lib/buildbot/buildbot_key" | ||
16 | LDAP_HOST = "ldap.immae.eu" | ||
17 | LDAP_DN = "cn=buildbot,ou=services,dc=immae,dc=eu" | ||
18 | LDAP_ROLES_BASE = "ou=roles,ou=hosts,dc=immae,dc=eu" | ||
19 | |||
20 | PUPPET_HOST = { | ||
21 | "production": "root@cryptoportfolio.immae.eu", | ||
22 | "integration": "root@cryptoportfolio-dev.immae.eu" | ||
23 | } | ||
24 | |||
25 | # master.cfg | ||
26 | SECRETS_FILE = os.getcwd() + "/secrets" | ||
27 | LDAP_URL = "ldaps://ldap.immae.eu:636" | ||
28 | LDAP_ADMIN_USER = "cn=buildbot,ou=services,dc=immae,dc=eu" | ||
29 | LDAP_BASE = "dc=immae,dc=eu" | ||
30 | LDAP_PATTERN = "(uid=%(username)s)" | ||
31 | LDAP_GROUP_PATTERN = "(&(memberOf=cn=groups,ou=cryptoportfolio,cn=buildbot,ou=services,dc=immae,dc=eu)(member=%(dn)s))" | ||
32 | TITLE_URL = "https://git.immae.eu" | ||
33 | TITLE = "Cryptoportfolio" | ||
34 | |||
35 | # eval .. dans .zshrc_local | ||
36 | # mkdir -p $BUILD/go | ||
37 | # export GOPATH=$BUILD/go | ||
38 | # go get -u github.com/golang/dep/cmd/dep | ||
39 | # export PATH=$PATH:$BUILD/go/bin | ||
40 | # go get git.immae.eu/Cryptoportfolio/Front.git | ||
41 | # cd $BUILD/go/src/git.immae.eu/Cryptoportfolio/Front.git | ||
42 | # git checkout dev | ||
43 | # dep ensure | ||
44 | def configure(c): | ||
45 | c["buildbotURL"] = E.BUILDBOT_URL | ||
46 | c["www"]["port"] = E.SOCKET | ||
47 | |||
48 | c['workers'].append(worker.LocalWorker("generic-worker")) | ||
49 | c['workers'].append(worker.LocalWorker("deploy-worker")) | ||
50 | |||
51 | c['schedulers'].append(hook_scheduler("Trader")) | ||
52 | c['schedulers'].append(hook_scheduler("Front")) | ||
53 | c['schedulers'].append(force_scheduler( | ||
54 | "force_cryptoportfolio", ["Trader_build", "Front_build"])) | ||
55 | c['schedulers'].append(deploy_scheduler("deploy_cryptoportfolio", | ||
56 | ["Trader_deploy", "Front_deploy"])) | ||
57 | |||
58 | c['builders'].append(factory("trader")) | ||
59 | c['builders'].append(factory("front", ignore_fails=True)) | ||
60 | |||
61 | c['builders'].append(deploy_factory("trader")) | ||
62 | c['builders'].append(deploy_factory("front")) | ||
63 | |||
64 | c['services'].append(SlackStatusPush( | ||
65 | name="slack_status_cryptoportfolio", | ||
66 | builders=["Front_build", "Trader_build", "Front_deploy", "Trader_deploy"], | ||
67 | serverUrl=open(E.SECRETS_FILE + "/slack_webhook", "r").read().rstrip())) | ||
68 | |||
69 | def factory(project, ignore_fails=False): | ||
70 | release_file = "{1}/{0}/{0}_%(kw:clean_branch)s.tar.gz" | ||
71 | |||
72 | url = E.GIT_URL.format(project.capitalize()) | ||
73 | |||
74 | package = util.Interpolate("{0}_%(kw:clean_branch)s.tar.gz".format(project), clean_branch=clean_branch) | ||
75 | package_dest = util.Interpolate(release_file.format(project, E.RELEASE_PATH), clean_branch=clean_branch) | ||
76 | package_url = util.Interpolate(release_file.format(project, E.RELEASE_URL), clean_branch=clean_branch) | ||
77 | |||
78 | factory = util.BuildFactory() | ||
79 | factory.addStep(steps.Git(logEnviron=False, repourl=url, | ||
80 | mode="full", method="copy")) | ||
81 | factory.addStep(steps.ShellCommand(name="make install", | ||
82 | logEnviron=False, haltOnFailure=(not ignore_fails), | ||
83 | warnOnFailure=ignore_fails, flunkOnFailure=(not ignore_fails), | ||
84 | command=["make", "install"])) | ||
85 | factory.addStep(steps.ShellCommand(name="make test", | ||
86 | logEnviron=False, haltOnFailure=(not ignore_fails), | ||
87 | warnOnFailure=ignore_fails, flunkOnFailure=(not ignore_fails), | ||
88 | command=["make", "test"])) | ||
89 | factory.addSteps(package_and_upload(package, package_dest, package_url)) | ||
90 | |||
91 | return util.BuilderConfig( | ||
92 | name="{}_build".format(project.capitalize()), | ||
93 | workernames=["generic-worker"], factory=factory) | ||
94 | |||
95 | def compute_build_infos(project): | ||
96 | @util.renderer | ||
97 | def compute(props): | ||
98 | import re, hashlib | ||
99 | build_file = props.getProperty("build") | ||
100 | package_dest = "{2}/{0}/{1}".format(project, build_file, E.RELEASE_PATH) | ||
101 | version = re.match(r"{0}_(.*).tar.gz".format(project), build_file).group(1) | ||
102 | with open(package_dest, "rb") as f: | ||
103 | sha = hashlib.sha256(f.read()).hexdigest() | ||
104 | return { | ||
105 | "build_version": version, | ||
106 | "build_hash": sha, | ||
107 | } | ||
108 | return compute | ||
109 | |||
110 | @util.renderer | ||
111 | def puppet_host(props): | ||
112 | environment = props["environment"] if props.hasProperty("environment") else "integration" | ||
113 | return E.PUPPET_HOST.get(environment, "host.invalid") | ||
114 | |||
115 | def deploy_factory(project): | ||
116 | package_dest = util.Interpolate("{1}/{0}/%(prop:build)s".format(project, E.RELEASE_PATH)) | ||
117 | |||
118 | factory = util.BuildFactory() | ||
119 | factory.addStep(steps.MasterShellCommand(command=["test", "-f", package_dest])) | ||
120 | factory.addStep(steps.SetProperties(properties=compute_build_infos(project))) | ||
121 | factory.addStep(LdapPush(environment=util.Property("environment"), | ||
122 | project=project, build_version=util.Property("build_version"), | ||
123 | build_hash=util.Property("build_hash"), ldap_password=util.Secret("ldap"))) | ||
124 | factory.addStep(steps.MasterShellCommand(command=[ | ||
125 | "ssh", "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "CheckHostIP=no", "-i", E.SSH_KEY_PATH, puppet_host])) | ||
126 | return util.BuilderConfig(name="{}_deploy".format(project.capitalize()), workernames=["deploy-worker"], factory=factory) | ||
127 | |||
128 | from twisted.internet import defer | ||
129 | from buildbot.process.buildstep import FAILURE | ||
130 | from buildbot.process.buildstep import SUCCESS | ||
131 | from buildbot.process.buildstep import BuildStep | ||
132 | |||
133 | class LdapPush(BuildStep): | ||
134 | name = "LdapPush" | ||
135 | renderables = ["environment", "project", "build_version", "build_hash", "ldap_password"] | ||
136 | |||
137 | def __init__(self, **kwargs): | ||
138 | self.environment = kwargs.pop("environment") | ||
139 | self.project = kwargs.pop("project") | ||
140 | self.build_version = kwargs.pop("build_version") | ||
141 | self.build_hash = kwargs.pop("build_hash") | ||
142 | self.ldap_password = kwargs.pop("ldap_password") | ||
143 | self.ldap_host = kwargs.pop("ldap_host", E.LDAP_HOST) | ||
144 | super().__init__(**kwargs) | ||
145 | |||
146 | def run(self): | ||
147 | import json | ||
148 | from ldap3 import Reader, Writer, Server, Connection, ObjectDef | ||
149 | server = Server(self.ldap_host) | ||
150 | conn = Connection(server, | ||
151 | user=E.LDAP_DN, | ||
152 | password=self.ldap_password) | ||
153 | conn.bind() | ||
154 | obj = ObjectDef("immaePuppetClass", conn) | ||
155 | r = Reader(conn, obj, | ||
156 | "cn=cryptoportfolio.{},{}".format(self.environment, E.LDAP_ROLES_BASE)) | ||
157 | r.search() | ||
158 | if len(r) > 0: | ||
159 | w = Writer.from_cursor(r) | ||
160 | for value in w[0].immaePuppetJson.values: | ||
161 | config = json.loads(value) | ||
162 | if "role::cryptoportfolio::{}_version".format(self.project) in config: | ||
163 | config["role::cryptoportfolio::{}_version".format(self.project)] = self.build_version | ||
164 | config["role::cryptoportfolio::{}_sha256".format(self.project)] = self.build_hash | ||
165 | w[0].immaePuppetJson -= value | ||
166 | w[0].immaePuppetJson += json.dumps(config, indent=" ") | ||
167 | w.commit() | ||
168 | return defer.succeed(SUCCESS) | ||
169 | return defer.succeed(FAILURE) | ||
diff --git a/modules/private/buildbot/projects/test/__init__.py b/modules/private/buildbot/projects/test/__init__.py new file mode 100644 index 0000000..e6b8d51 --- /dev/null +++ b/modules/private/buildbot/projects/test/__init__.py | |||
@@ -0,0 +1,188 @@ | |||
1 | from buildbot.plugins import * | ||
2 | from buildbot_common.build_helpers import * | ||
3 | import os | ||
4 | from buildbot.util import bytes2unicode | ||
5 | import json | ||
6 | |||
7 | __all__ = [ "configure", "E" ] | ||
8 | |||
9 | class E(): | ||
10 | PROJECT = "test" | ||
11 | BUILDBOT_URL = "https://git.immae.eu/buildbot/{}/".format(PROJECT) | ||
12 | SOCKET = "unix:/run/buildbot/{}.sock".format(PROJECT) | ||
13 | PB_SOCKET = "unix:address=/run/buildbot/{}_pb.sock".format(PROJECT) | ||
14 | RELEASE_PATH = "/var/lib/ftp/release.immae.eu/{}".format(PROJECT) | ||
15 | RELEASE_URL = "https://release.immae.eu/{}".format(PROJECT) | ||
16 | GIT_URL = "https://git.immae.eu/perso/Immae/TestProject.git" | ||
17 | SSH_KEY_PATH = "/var/lib/buildbot/buildbot_key" | ||
18 | PUPPET_HOST = "root@backup-1.v.immae.eu" | ||
19 | LDAP_HOST = "ldap.immae.eu" | ||
20 | LDAP_DN = "cn=buildbot,ou=services,dc=immae,dc=eu" | ||
21 | LDAP_ROLES_BASE = "ou=roles,ou=hosts,dc=immae,dc=eu" | ||
22 | XMPP_RECIPIENTS = os.environ["BUILDBOT_XMPP_RECIPIENTS"].split(" ") | ||
23 | |||
24 | # master.cfg | ||
25 | SECRETS_FILE = os.getcwd() + "/secrets" | ||
26 | LDAP_URL = "ldaps://ldap.immae.eu:636" | ||
27 | LDAP_ADMIN_USER = "cn=buildbot,ou=services,dc=immae,dc=eu" | ||
28 | LDAP_BASE = "dc=immae,dc=eu" | ||
29 | LDAP_PATTERN = "(uid=%(username)s)" | ||
30 | LDAP_GROUP_PATTERN = "(&(memberOf=cn=groups,ou=test,cn=buildbot,ou=services,dc=immae,dc=eu)(member=%(dn)s))" | ||
31 | TITLE_URL = "https://git.immae.eu/?p=perso/Immae/TestProject.git;a=summary" | ||
32 | TITLE = "Test project" | ||
33 | |||
34 | class CustomBase(webhooks.base): | ||
35 | def getChanges(self, request): | ||
36 | try: | ||
37 | content = request.content.read() | ||
38 | args = json.loads(bytes2unicode(content)) | ||
39 | except Exception as e: | ||
40 | raise ValueError("Error loading JSON: " + str(e)) | ||
41 | |||
42 | args.setdefault("comments", "") | ||
43 | args.setdefault("repository", "") | ||
44 | args.setdefault("author", args.get("who", "unknown")) | ||
45 | |||
46 | if args["category"] == "deploy_webhook": | ||
47 | args = { | ||
48 | "category": "deploy_webhook", | ||
49 | "comments": "", | ||
50 | "repository": "", | ||
51 | "author": "unknown", | ||
52 | "project": "TestProject", | ||
53 | "properties": { | ||
54 | "environment": args.get("environment", "integration"), | ||
55 | "build": "test_{}.tar.gz".format(args.get("branch", "master")) | ||
56 | } | ||
57 | } | ||
58 | |||
59 | return ([args], None) | ||
60 | |||
61 | def deploy_hook_scheduler(project, timer=1): | ||
62 | return schedulers.AnyBranchScheduler( | ||
63 | change_filter=util.ChangeFilter(category="deploy_webhook", project=project), | ||
64 | name="{}_deploy".format(project), treeStableTimer=timer, builderNames=["{}_deploy".format(project)]) | ||
65 | |||
66 | def configure(c): | ||
67 | c["buildbotURL"] = E.BUILDBOT_URL | ||
68 | c["www"]["port"] = E.SOCKET | ||
69 | |||
70 | c["www"]["change_hook_dialects"]["base"] = { | ||
71 | "custom_class": CustomBase | ||
72 | } | ||
73 | c['workers'].append(worker.LocalWorker("generic-worker-test")) | ||
74 | c['workers'].append(worker.LocalWorker("deploy-worker-test")) | ||
75 | |||
76 | c['schedulers'].append(hook_scheduler("TestProject", timer=1)) | ||
77 | c['schedulers'].append(force_scheduler("force_test", ["TestProject_build"])) | ||
78 | c['schedulers'].append(deploy_scheduler("deploy_test", ["TestProject_deploy"])) | ||
79 | c['schedulers'].append(deploy_hook_scheduler("TestProject", timer=1)) | ||
80 | |||
81 | c['builders'].append(factory()) | ||
82 | c['builders'].append(deploy_factory()) | ||
83 | |||
84 | c['services'].append(SlackStatusPush( | ||
85 | name="slack_status_test_project", | ||
86 | builders=["TestProject_build", "TestProject_deploy"], | ||
87 | serverUrl=open(E.SECRETS_FILE + "/slack_webhook", "r").read().rstrip())) | ||
88 | c['services'].append(XMPPStatusPush( | ||
89 | name="xmpp_status_test_project", | ||
90 | builders=["TestProject_build", "TestProject_deploy"], | ||
91 | recipients=E.XMPP_RECIPIENTS, | ||
92 | password=open(E.SECRETS_FILE + "/notify_xmpp_password", "r").read().rstrip())) | ||
93 | |||
94 | def factory(): | ||
95 | package = util.Interpolate("test_%(kw:clean_branch)s.tar.gz", clean_branch=clean_branch) | ||
96 | package_dest = util.Interpolate("{}/test_%(kw:clean_branch)s.tar.gz".format(E.RELEASE_PATH), clean_branch=clean_branch) | ||
97 | package_url = util.Interpolate("{}/test_%(kw:clean_branch)s.tar.gz".format(E.RELEASE_URL), clean_branch=clean_branch) | ||
98 | |||
99 | factory = util.BuildFactory() | ||
100 | factory.addStep(steps.Git(logEnviron=False, | ||
101 | repourl=E.GIT_URL, mode="full", method="copy")) | ||
102 | factory.addStep(steps.ShellCommand(name="env", | ||
103 | logEnviron=False, command=["env"])) | ||
104 | factory.addStep(steps.ShellCommand(name="pwd", | ||
105 | logEnviron=False, command=["pwd"])) | ||
106 | factory.addStep(steps.ShellCommand(name="true", | ||
107 | logEnviron=False, command=["true"])) | ||
108 | factory.addStep(steps.ShellCommand(name="echo", | ||
109 | logEnviron=False, command=["echo", package])) | ||
110 | factory.addSteps(package_and_upload(package, package_dest, package_url)) | ||
111 | |||
112 | return util.BuilderConfig(name="TestProject_build", workernames=["generic-worker-test"], factory=factory) | ||
113 | |||
114 | |||
115 | def compute_build_infos(): | ||
116 | @util.renderer | ||
117 | def compute(props): | ||
118 | import re, hashlib | ||
119 | build_file = props.getProperty("build") | ||
120 | package_dest = "{}/{}".format(E.RELEASE_PATH, build_file) | ||
121 | version = re.match(r"{0}_(.*).tar.gz".format("test"), build_file).group(1) | ||
122 | with open(package_dest, "rb") as f: | ||
123 | sha = hashlib.sha256(f.read()).hexdigest() | ||
124 | return { | ||
125 | "build_version": version, | ||
126 | "build_hash": sha, | ||
127 | } | ||
128 | return compute | ||
129 | |||
130 | @util.renderer | ||
131 | def puppet_host(props): | ||
132 | return E.PUPPET_HOST | ||
133 | |||
134 | def deploy_factory(): | ||
135 | package_dest = util.Interpolate("{}/%(prop:build)s".format(E.RELEASE_PATH)) | ||
136 | |||
137 | factory = util.BuildFactory() | ||
138 | factory.addStep(steps.MasterShellCommand(command=["test", "-f", package_dest])) | ||
139 | factory.addStep(steps.SetProperties(properties=compute_build_infos())) | ||
140 | factory.addStep(LdapPush(environment=util.Property("environment"), | ||
141 | build_version=util.Property("build_version"), | ||
142 | build_hash=util.Property("build_hash"), | ||
143 | ldap_password=util.Secret("ldap"))) | ||
144 | factory.addStep(steps.MasterShellCommand(command=[ | ||
145 | "ssh", "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "CheckHostIP=no", "-i", E.SSH_KEY_PATH, puppet_host])) | ||
146 | return util.BuilderConfig(name="TestProject_deploy", workernames=["deploy-worker-test"], factory=factory) | ||
147 | |||
148 | from twisted.internet import defer | ||
149 | from buildbot.process.buildstep import FAILURE | ||
150 | from buildbot.process.buildstep import SUCCESS | ||
151 | from buildbot.process.buildstep import BuildStep | ||
152 | |||
153 | class LdapPush(BuildStep): | ||
154 | name = "LdapPush" | ||
155 | renderables = ["environment", "build_version", "build_hash", "ldap_password"] | ||
156 | |||
157 | def __init__(self, **kwargs): | ||
158 | self.environment = kwargs.pop("environment") | ||
159 | self.build_version = kwargs.pop("build_version") | ||
160 | self.build_hash = kwargs.pop("build_hash") | ||
161 | self.ldap_password = kwargs.pop("ldap_password") | ||
162 | self.ldap_host = kwargs.pop("ldap_host", E.LDAP_HOST) | ||
163 | super().__init__(**kwargs) | ||
164 | |||
165 | def run(self): | ||
166 | import json | ||
167 | from ldap3 import Reader, Writer, Server, Connection, ObjectDef | ||
168 | server = Server(self.ldap_host) | ||
169 | conn = Connection(server, | ||
170 | user=E.LDAP_DN, | ||
171 | password=self.ldap_password) | ||
172 | conn.bind() | ||
173 | obj = ObjectDef("immaePuppetClass", conn) | ||
174 | r = Reader(conn, obj, | ||
175 | "cn=test.{},{}".format(self.environment, E.LDAP_ROLES_BASE)) | ||
176 | r.search() | ||
177 | if len(r) > 0: | ||
178 | w = Writer.from_cursor(r) | ||
179 | for value in w[0].immaePuppetJson.values: | ||
180 | config = json.loads(value) | ||
181 | if "test_version" in config: | ||
182 | config["test_version"] = self.build_version | ||
183 | config["test_sha256"] = self.build_hash | ||
184 | w[0].immaePuppetJson -= value | ||
185 | w[0].immaePuppetJson += json.dumps(config, indent=" ") | ||
186 | w.commit() | ||
187 | return defer.succeed(SUCCESS) | ||
188 | return defer.succeed(FAILURE) | ||
diff --git a/modules/private/certificates.nix b/modules/private/certificates.nix new file mode 100644 index 0000000..43f6a23 --- /dev/null +++ b/modules/private/certificates.nix | |||
@@ -0,0 +1,52 @@ | |||
1 | { lib, pkgs, config, ... }: | ||
2 | { | ||
3 | options.services.myCertificates = { | ||
4 | certConfig = lib.mkOption { | ||
5 | default = { | ||
6 | webroot = "${config.security.acme.directory}/acme-challenge"; | ||
7 | email = "ismael@bouya.org"; | ||
8 | postRun = '' | ||
9 | systemctl reload httpdTools.service httpdInte.service httpdProd.service | ||
10 | ''; | ||
11 | plugins = [ "cert.pem" "chain.pem" "fullchain.pem" "full.pem" "key.pem" "account_key.json" ]; | ||
12 | }; | ||
13 | description = "Default configuration for certificates"; | ||
14 | }; | ||
15 | }; | ||
16 | |||
17 | config = { | ||
18 | services.websitesCerts = config.services.myCertificates.certConfig; | ||
19 | myServices.databasesCerts = config.services.myCertificates.certConfig; | ||
20 | myServices.ircCerts = config.services.myCertificates.certConfig; | ||
21 | |||
22 | security.acme.preliminarySelfsigned = true; | ||
23 | |||
24 | security.acme.certs = { | ||
25 | "eldiron" = config.services.myCertificates.certConfig // { | ||
26 | domain = "eldiron.immae.eu"; | ||
27 | }; | ||
28 | }; | ||
29 | |||
30 | systemd.services = lib.attrsets.mapAttrs' (k: v: | ||
31 | lib.attrsets.nameValuePair "acme-selfsigned-${k}" (lib.mkBefore { script = | ||
32 | (lib.optionalString (builtins.elem "cert.pem" v.plugins) '' | ||
33 | cp $workdir/server.crt ${config.security.acme.directory}/${k}/cert.pem | ||
34 | chown '${v.user}:${v.group}' ${config.security.acme.directory}/${k}/cert.pem | ||
35 | chmod ${if v.allowKeysForGroup then "750" else "700"} ${config.security.acme.directory}/${k}/cert.pem | ||
36 | '') + | ||
37 | (lib.optionalString (builtins.elem "chain.pem" v.plugins) '' | ||
38 | cp $workdir/ca.crt ${config.security.acme.directory}/${k}/chain.pem | ||
39 | chown '${v.user}:${v.group}' ${config.security.acme.directory}/${k}/chain.pem | ||
40 | chmod ${if v.allowKeysForGroup then "750" else "700"} ${config.security.acme.directory}/${k}/chain.pem | ||
41 | '') | ||
42 | ; }) | ||
43 | ) config.security.acme.certs // { | ||
44 | httpdProd.after = [ "acme-selfsigned-certificates.target" ]; | ||
45 | httpdProd.wants = [ "acme-selfsigned-certificates.target" ]; | ||
46 | httpdTools.after = [ "acme-selfsigned-certificates.target" ]; | ||
47 | httpdTools.wants = [ "acme-selfsigned-certificates.target" ]; | ||
48 | httpdInte.after = [ "acme-selfsigned-certificates.target" ]; | ||
49 | httpdInte.wants = [ "acme-selfsigned-certificates.target" ]; | ||
50 | }; | ||
51 | }; | ||
52 | } | ||
diff --git a/modules/private/default.nix b/modules/private/default.nix index 242eeb9..894efb7 100644 --- a/modules/private/default.nix +++ b/modules/private/default.nix | |||
@@ -47,7 +47,19 @@ set = { | |||
47 | peertubeTool = ./websites/tools/peertube; | 47 | peertubeTool = ./websites/tools/peertube; |
48 | toolsTool = ./websites/tools/tools; | 48 | toolsTool = ./websites/tools/tools; |
49 | 49 | ||
50 | buildbot = ./buildbot; | ||
51 | certificates = ./certificates.nix; | ||
52 | gitolite = ./gitolite; | ||
50 | irc = ./irc.nix; | 53 | irc = ./irc.nix; |
54 | pub = ./pub; | ||
55 | tasks = ./tasks; | ||
56 | dns = ./dns.nix; | ||
57 | ftp = ./ftp.nix; | ||
58 | mail = ./mail.nix; | ||
59 | mpd = ./mpd.nix; | ||
60 | ssh = ./ssh; | ||
61 | |||
62 | system = ./system.nix; | ||
51 | }; | 63 | }; |
52 | in | 64 | in |
53 | builtins.listToAttrs (map (attr: { name = "priv${attr}"; value = set.${attr}; }) (builtins.attrNames set)) | 65 | builtins.listToAttrs (map (attr: { name = "priv${attr}"; value = set.${attr}; }) (builtins.attrNames set)) |
diff --git a/modules/private/dns.nix b/modules/private/dns.nix new file mode 100644 index 0000000..ced8d9b --- /dev/null +++ b/modules/private/dns.nix | |||
@@ -0,0 +1,132 @@ | |||
1 | { lib, pkgs, config, myconfig, ... }: | ||
2 | { | ||
3 | config = let | ||
4 | cfg = config.services.bind; | ||
5 | configFile = pkgs.writeText "named.conf" '' | ||
6 | include "/etc/bind/rndc.key"; | ||
7 | controls { | ||
8 | inet 127.0.0.1 allow {localhost;} keys {"rndc-key";}; | ||
9 | }; | ||
10 | |||
11 | acl cachenetworks { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.cacheNetworks} }; | ||
12 | acl badnetworks { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.blockedNetworks} }; | ||
13 | |||
14 | options { | ||
15 | listen-on { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.listenOn} }; | ||
16 | listen-on-v6 { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.listenOnIpv6} }; | ||
17 | allow-query { cachenetworks; }; | ||
18 | blackhole { badnetworks; }; | ||
19 | forward first; | ||
20 | forwarders { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.forwarders} }; | ||
21 | directory "/var/run/named"; | ||
22 | pid-file "/var/run/named/named.pid"; | ||
23 | ${cfg.extraOptions} | ||
24 | }; | ||
25 | |||
26 | ${cfg.extraConfig} | ||
27 | |||
28 | ${ lib.concatMapStrings | ||
29 | ({ name, file, master ? true, extra ? "", slaves ? [], masters ? [] }: | ||
30 | '' | ||
31 | zone "${name}" { | ||
32 | type ${if master then "master" else "slave"}; | ||
33 | file "${file}"; | ||
34 | ${ if lib.lists.length slaves > 0 then | ||
35 | '' | ||
36 | allow-transfer { | ||
37 | ${lib.concatMapStrings (ip: "${ip};\n") slaves} | ||
38 | }; | ||
39 | '' else ""} | ||
40 | ${ if lib.lists.length masters > 0 then | ||
41 | '' | ||
42 | masters { | ||
43 | ${lib.concatMapStrings (ip: "${ip};\n") masters} | ||
44 | }; | ||
45 | '' else ""} | ||
46 | allow-query { any; }; | ||
47 | ${extra} | ||
48 | }; | ||
49 | '') | ||
50 | cfg.zones } | ||
51 | ''; | ||
52 | in | ||
53 | { | ||
54 | networking.firewall.allowedUDPPorts = [ 53 ]; | ||
55 | networking.firewall.allowedTCPPorts = [ 53 ]; | ||
56 | services.bind = { | ||
57 | enable = true; | ||
58 | cacheNetworks = ["any"]; | ||
59 | configFile = configFile; | ||
60 | extraOptions = '' | ||
61 | allow-recursion { 127.0.0.1; }; | ||
62 | allow-transfer { none; }; | ||
63 | |||
64 | notify-source ${myconfig.env.servers.eldiron.ips.main.ip4}; | ||
65 | notify-source-v6 ${lib.head myconfig.env.servers.eldiron.ips.main.ip6}; | ||
66 | version none; | ||
67 | hostname none; | ||
68 | server-id none; | ||
69 | ''; | ||
70 | zones = with myconfig.env.dns; | ||
71 | assert (builtins.substring ((builtins.stringLength soa.email)-1) 1 soa.email) != "."; | ||
72 | assert (builtins.substring ((builtins.stringLength soa.primary)-1) 1 soa.primary) != "."; | ||
73 | (map (conf: { | ||
74 | name = conf.name; | ||
75 | master = false; | ||
76 | file = "/var/run/named/${conf.name}.zone"; | ||
77 | masters = if lib.attrsets.hasAttr "masters" conf | ||
78 | then lib.lists.flatten (map (n: lib.attrsets.attrValues ns.${n}) conf.masters) | ||
79 | else []; | ||
80 | }) slaveZones) | ||
81 | ++ (map (conf: { | ||
82 | name = conf.name; | ||
83 | master = true; | ||
84 | extra = if lib.attrsets.hasAttr "extra" conf then conf.extra else ""; | ||
85 | slaves = if lib.attrsets.hasAttr "slaves" conf | ||
86 | then lib.lists.flatten (map (n: lib.attrsets.attrValues ns.${n}) conf.slaves) | ||
87 | else []; | ||
88 | file = pkgs.writeText "${conf.name}.zone" '' | ||
89 | $TTL 10800 | ||
90 | @ IN SOA ${soa.primary}. ${builtins.replaceStrings ["@"] ["."] soa.email}. ${soa.serial} ${soa.refresh} ${soa.retry} ${soa.expire} ${soa.ttl} | ||
91 | |||
92 | ${lib.concatStringsSep "\n" (map (x: "@ IN NS ${x}.") (lib.concatMap (n: lib.attrsets.mapAttrsToList (k: v: k) ns.${n}) conf.ns))} | ||
93 | |||
94 | ${conf.entries} | ||
95 | |||
96 | ${if lib.attrsets.hasAttr "withEmail" conf && lib.lists.length conf.withEmail > 0 then '' | ||
97 | mail IN A ${myconfig.env.servers.immaeEu.ips.main.ip4} | ||
98 | mx-1 IN A ${myconfig.env.servers.eldiron.ips.main.ip4} | ||
99 | ${builtins.concatStringsSep "\n" (map (i: "mail IN AAAA ${i}") myconfig.env.servers.immaeEu.ips.main.ip6)} | ||
100 | ${builtins.concatStringsSep "\n" (map (i: "mx-1 IN AAAA ${i}") myconfig.env.servers.eldiron.ips.main.ip6)} | ||
101 | ${lib.concatStringsSep "\n\n" (map (e: | ||
102 | let | ||
103 | n = if e.domain == "" then "@" else "${e.domain} "; | ||
104 | suffix = if e.domain == "" then "" else ".${e.domain}"; | ||
105 | in | ||
106 | '' | ||
107 | ; ------------------ mail: ${n} --------------------------- | ||
108 | ${if e.receive then "${n} IN MX 10 mail.${conf.name}." else ""} | ||
109 | ;${if e.receive then "${n} IN MX 50 mx-1.${conf.name}." else ""} | ||
110 | |||
111 | ; Mail sender authentications | ||
112 | ${n} IN TXT "v=spf1 mx ~all" | ||
113 | _dmarc${suffix} IN TXT "v=DMARC1; p=none; adkim=r; aspf=r; fo=1; rua=mailto:postmaster+rua@immae.eu; ruf=mailto:postmaster+ruf@immae.eu;" | ||
114 | ${if e.send then '' | ||
115 | immae_eu._domainkey${suffix} IN TXT ( "v=DKIM1; k=rsa; s=email; " | ||
116 | "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzl3vLd8W5YAuumC5+ZT9OV7/14Pmh5JYtwyqKI3cfe9NnAqInt3xO4bZ7oqIxRKWN4SD39vm7O/QOvFdBt00ENOOzdP90s5gKw6eIP/4+vPTh0IWltAsmu9B2agzdtWUE7t2xFKIzEn8l9niRE2QYbVaqZv4sub98vY55fIgFoHtjkmNC7325S8fjDJGp6OPbyhAs6Xl5/adjF" | ||
117 | "0ko4Y2p6RaxLQfjlS0bxmK4Qg6C14pIXHtzVeqOuWrwApqt5+AULSn97iUtqV/IJlEEjC6DUR44t3C/G0G/k46iFclCqRRi0hdPrOHCtZDbtMubnTN9eaUiNpkXh1WnCflHwtjQwIDAQAB" ) | ||
118 | '' else ""} | ||
119 | '') conf.withEmail)} | ||
120 | '' + (if conf.name == "immae.eu" then '' | ||
121 | ; ----------------- Accept DMARC reports ------------------- | ||
122 | ${lib.concatStringsSep "\n" ( | ||
123 | lib.flatten ( | ||
124 | map (z: map (e: "${e.domain}${if builtins.stringLength e.domain > 0 then "." else ""}${z.name}._report._dmarc IN TXT \"v=DMARC1;\"") (z.withEmail or [])) masterZones | ||
125 | ) | ||
126 | )} | ||
127 | '' else "") else ""} | ||
128 | ''; | ||
129 | }) masterZones); | ||
130 | }; | ||
131 | }; | ||
132 | } | ||
diff --git a/modules/private/ftp.nix b/modules/private/ftp.nix new file mode 100644 index 0000000..842d2d6 --- /dev/null +++ b/modules/private/ftp.nix | |||
@@ -0,0 +1,118 @@ | |||
1 | { lib, pkgs, config, myconfig, ... }: | ||
2 | { | ||
3 | options = { | ||
4 | services.pure-ftpd.enable = lib.mkOption { | ||
5 | type = lib.types.bool; | ||
6 | default = false; | ||
7 | description = '' | ||
8 | Whether to enable pure-ftpd. | ||
9 | ''; | ||
10 | }; | ||
11 | }; | ||
12 | |||
13 | config = lib.mkIf config.services.pure-ftpd.enable { | ||
14 | security.acme.certs."ftp" = config.services.myCertificates.certConfig // { | ||
15 | domain = "eldiron.immae.eu"; | ||
16 | postRun = '' | ||
17 | systemctl restart pure-ftpd.service | ||
18 | ''; | ||
19 | extraDomains = { "ftp.immae.eu" = null; }; | ||
20 | }; | ||
21 | |||
22 | networking = { | ||
23 | firewall = { | ||
24 | allowedTCPPorts = [ 21 ]; | ||
25 | allowedTCPPortRanges = [ { from = 40000; to = 50000; } ]; | ||
26 | }; | ||
27 | }; | ||
28 | |||
29 | users.users = [ | ||
30 | { | ||
31 | name = "ftp"; | ||
32 | uid = config.ids.uids.ftp; # 8 | ||
33 | group = "ftp"; | ||
34 | description = "Anonymous FTP user"; | ||
35 | home = "/homeless-shelter"; | ||
36 | extraGroups = [ "keys" ]; | ||
37 | } | ||
38 | ]; | ||
39 | |||
40 | users.groups.ftp.gid = config.ids.gids.ftp; | ||
41 | |||
42 | system.activationScripts.pure-ftpd = '' | ||
43 | install -m 0755 -o ftp -g ftp -d /var/lib/ftp | ||
44 | ''; | ||
45 | |||
46 | secrets.keys = [{ | ||
47 | dest = "pure-ftpd-ldap"; | ||
48 | permissions = "0400"; | ||
49 | user = "ftp"; | ||
50 | group = "ftp"; | ||
51 | text = '' | ||
52 | LDAPServer ${myconfig.env.ftp.ldap.host} | ||
53 | LDAPPort 389 | ||
54 | LDAPUseTLS True | ||
55 | LDAPBaseDN ${myconfig.env.ftp.ldap.base} | ||
56 | LDAPBindDN ${myconfig.env.ftp.ldap.dn} | ||
57 | LDAPBindPW ${myconfig.env.ftp.ldap.password} | ||
58 | LDAPDefaultUID 500 | ||
59 | LDAPForceDefaultUID False | ||
60 | LDAPDefaultGID 100 | ||
61 | LDAPForceDefaultGID False | ||
62 | LDAPFilter ${myconfig.env.ftp.ldap.filter} | ||
63 | |||
64 | LDAPAuthMethod BIND | ||
65 | |||
66 | # Pas de possibilite de donner l'Uid/Gid ! | ||
67 | # Compile dans pure-ftpd directement avec immaeFtpUid / immaeFtpGid | ||
68 | LDAPHomeDir immaeFtpDirectory | ||
69 | ''; | ||
70 | }]; | ||
71 | |||
72 | systemd.services.pure-ftpd = let | ||
73 | configFile = pkgs.writeText "pure-ftpd.conf" '' | ||
74 | PassivePortRange 40000 50000 | ||
75 | ChrootEveryone yes | ||
76 | CreateHomeDir yes | ||
77 | BrokenClientsCompatibility yes | ||
78 | MaxClientsNumber 50 | ||
79 | Daemonize yes | ||
80 | MaxClientsPerIP 8 | ||
81 | VerboseLog no | ||
82 | DisplayDotFiles yes | ||
83 | AnonymousOnly no | ||
84 | NoAnonymous no | ||
85 | SyslogFacility ftp | ||
86 | DontResolve yes | ||
87 | MaxIdleTime 15 | ||
88 | LDAPConfigFile /var/secrets/pure-ftpd-ldap | ||
89 | LimitRecursion 10000 8 | ||
90 | AnonymousCanCreateDirs no | ||
91 | MaxLoad 4 | ||
92 | AntiWarez yes | ||
93 | Umask 133:022 | ||
94 | # ftp | ||
95 | MinUID 8 | ||
96 | AllowUserFXP no | ||
97 | AllowAnonymousFXP no | ||
98 | ProhibitDotFilesWrite no | ||
99 | ProhibitDotFilesRead no | ||
100 | AutoRename no | ||
101 | AnonymousCantUpload no | ||
102 | MaxDiskUsage 99 | ||
103 | CustomerProof yes | ||
104 | TLS 1 | ||
105 | CertFile ${config.security.acme.directory}/ftp/full.pem | ||
106 | ''; | ||
107 | in { | ||
108 | description = "Pure-FTPd server"; | ||
109 | wantedBy = [ "multi-user.target" ]; | ||
110 | after = [ "network.target" ]; | ||
111 | |||
112 | serviceConfig.ExecStart = "${pkgs.pure-ftpd}/bin/pure-ftpd ${configFile}"; | ||
113 | serviceConfig.Type = "forking"; | ||
114 | serviceConfig.PIDFile = "/run/pure-ftpd.pid"; | ||
115 | }; | ||
116 | }; | ||
117 | |||
118 | } | ||
diff --git a/modules/private/gitolite/default.nix b/modules/private/gitolite/default.nix new file mode 100644 index 0000000..b9914a1 --- /dev/null +++ b/modules/private/gitolite/default.nix | |||
@@ -0,0 +1,63 @@ | |||
1 | { lib, pkgs, config, myconfig, ... }: | ||
2 | let | ||
3 | cfg = config.myServices.gitolite; | ||
4 | in { | ||
5 | options.myServices.gitolite = { | ||
6 | enable = lib.mkEnableOption "my gitolite service"; | ||
7 | gitoliteDir = lib.mkOption { | ||
8 | type = lib.types.string; | ||
9 | default = "/var/lib/gitolite"; | ||
10 | }; | ||
11 | }; | ||
12 | |||
13 | config = lib.mkIf cfg.enable { | ||
14 | networking.firewall.allowedTCPPorts = [ 9418 ]; | ||
15 | |||
16 | services.gitDaemon = { | ||
17 | enable = true; | ||
18 | user = "gitolite"; | ||
19 | group = "gitolite"; | ||
20 | basePath = "${cfg.gitoliteDir}/repositories"; | ||
21 | }; | ||
22 | |||
23 | system.activationScripts.gitolite = let | ||
24 | gitolite_ldap_groups = pkgs.mylibs.wrap { | ||
25 | name = "gitolite_ldap_groups.sh"; | ||
26 | file = ./gitolite_ldap_groups.sh; | ||
27 | vars = { | ||
28 | LDAP_PASS = myconfig.env.tools.gitolite.ldap.password; | ||
29 | }; | ||
30 | paths = [ pkgs.openldap pkgs.stdenv.shellPackage pkgs.gnugrep pkgs.coreutils ]; | ||
31 | }; | ||
32 | in { | ||
33 | deps = [ "users" ]; | ||
34 | text = '' | ||
35 | if [ -d ${cfg.gitoliteDir} ]; then | ||
36 | ln -sf ${gitolite_ldap_groups} ${cfg.gitoliteDir}/gitolite_ldap_groups.sh | ||
37 | chmod g+rx ${cfg.gitoliteDir} | ||
38 | fi | ||
39 | if [ -f ${cfg.gitoliteDir}/projects.list ]; then | ||
40 | chmod g+r ${cfg.gitoliteDir}/projects.list | ||
41 | fi | ||
42 | ''; | ||
43 | }; | ||
44 | |||
45 | users.users.wwwrun.extraGroups = [ "gitolite" ]; | ||
46 | |||
47 | users.users.gitolite.packages = let | ||
48 | python-packages = python-packages: with python-packages; [ | ||
49 | simplejson | ||
50 | urllib3 | ||
51 | sleekxmpp | ||
52 | ]; | ||
53 | in | ||
54 | [ | ||
55 | (pkgs.python3.withPackages python-packages) | ||
56 | ]; | ||
57 | # Installation: https://git.immae.eu/mantisbt/view.php?id=93 | ||
58 | services.gitolite = { | ||
59 | enable = true; | ||
60 | adminPubkey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDXqRbiHw7QoHADNIEuo4nUT9fSOIEBMdJZH0bkQAxXyJFyCM1IMz0pxsHV0wu9tdkkr36bPEUj2aV5bkYLBN6nxcV2Y49X8bjOSCPfx3n6Own1h+NeZVBj4ZByrFmqCbTxUJIZ2bZKcWOFncML39VmWdsVhNjg0X4NBBehqXRIKr2gt3E/ESAxTYJFm0BnU0baciw9cN0bsRGqvFgf5h2P48CIAfwhVcGmPQnnAwabnosYQzRWxR0OygH5Kd8mePh6FheIRIigfXsDO8f/jdxwut8buvNIf3m5EBr3tUbTsvM+eV3M5vKGt7sk8T64DVtepTSdOOWtp+47ktsnHOMh immae@immae.eu"; | ||
61 | }; | ||
62 | }; | ||
63 | } | ||
diff --git a/modules/private/gitolite/gitolite_ldap_groups.sh b/modules/private/gitolite/gitolite_ldap_groups.sh new file mode 100755 index 0000000..7db0da4 --- /dev/null +++ b/modules/private/gitolite/gitolite_ldap_groups.sh | |||
@@ -0,0 +1,15 @@ | |||
1 | #!/usr/bin/env bash | ||
2 | |||
3 | uid_param="$1" | ||
4 | ldap_host="ldap.immae.eu" | ||
5 | ldap_binddn="cn=gitolite,ou=services,dc=immae,dc=eu" | ||
6 | ldap_bindpw="$LDAP_PASS" | ||
7 | ldap_searchbase="dc=immae,dc=eu" | ||
8 | ldap_scope="subtree" | ||
9 | |||
10 | ldap_options="-h ${ldap_host} -ZZ -x -D ${ldap_binddn} -w ${ldap_bindpw} -b ${ldap_searchbase} -s ${ldap_scope}" | ||
11 | |||
12 | ldap_filter="(&(memberOf=cn=groups,cn=gitolite,ou=services,dc=immae,dc=eu)(|(member=uid=${uid_param},ou=users,dc=immae,dc=eu)(member=uid=${uid_param},ou=group_users,dc=immae,dc=eu)))" | ||
13 | ldap_result=$(ldapsearch ${ldap_options} -LLL "${ldap_filter}" cn | grep 'cn:' | cut -d' ' -f2) | ||
14 | |||
15 | echo "$ldap_result" | ||
diff --git a/modules/private/mail.nix b/modules/private/mail.nix new file mode 100644 index 0000000..611c8b4 --- /dev/null +++ b/modules/private/mail.nix | |||
@@ -0,0 +1,13 @@ | |||
1 | { lib, pkgs, config, myconfig, ... }: | ||
2 | { | ||
3 | config.users.users.nullmailer.uid = config.ids.uids.nullmailer; | ||
4 | config.users.groups.nullmailer.gid = config.ids.gids.nullmailer; | ||
5 | |||
6 | config.services.nullmailer = { | ||
7 | enable = true; | ||
8 | config = { | ||
9 | me = myconfig.env.mail.host; | ||
10 | remotes = "${myconfig.env.mail.relay} smtp"; | ||
11 | }; | ||
12 | }; | ||
13 | } | ||
diff --git a/modules/private/mpd.nix b/modules/private/mpd.nix new file mode 100644 index 0000000..9903bdf --- /dev/null +++ b/modules/private/mpd.nix | |||
@@ -0,0 +1,56 @@ | |||
1 | { lib, pkgs, config, myconfig, ... }: | ||
2 | { | ||
3 | config = { | ||
4 | secrets.keys = [ | ||
5 | { | ||
6 | dest = "mpd"; | ||
7 | permissions = "0400"; | ||
8 | text = myconfig.env.mpd.password; | ||
9 | } | ||
10 | { | ||
11 | dest = "mpd-config"; | ||
12 | permissions = "0400"; | ||
13 | user = "mpd"; | ||
14 | group = "mpd"; | ||
15 | text = '' | ||
16 | password "${myconfig.env.mpd.password}@read,add,control,admin" | ||
17 | ''; | ||
18 | } | ||
19 | ]; | ||
20 | networking.firewall.allowedTCPPorts = [ 6600 ]; | ||
21 | users.users.mpd.extraGroups = [ "wwwrun" "keys" ]; | ||
22 | systemd.services.mpd.serviceConfig.RuntimeDirectory = "mpd"; | ||
23 | services.mpd = { | ||
24 | enable = true; | ||
25 | network.listenAddress = "any"; | ||
26 | musicDirectory = myconfig.env.mpd.folder; | ||
27 | extraConfig = '' | ||
28 | include "/var/secrets/mpd-config" | ||
29 | audio_output { | ||
30 | type "null" | ||
31 | name "No Output" | ||
32 | mixer_type "none" | ||
33 | } | ||
34 | audio_output { | ||
35 | type "httpd" | ||
36 | name "OGG" | ||
37 | encoder "vorbis" | ||
38 | bind_to_address "/run/mpd/ogg.sock" | ||
39 | quality "5.0" | ||
40 | format "44100:16:1" | ||
41 | } | ||
42 | audio_output { | ||
43 | type "httpd" | ||
44 | name "MP3" | ||
45 | encoder "lame" | ||
46 | bind_to_address "/run/mpd/mp3.sock" | ||
47 | quality "5.0" | ||
48 | format "44100:16:1" | ||
49 | } | ||
50 | |||
51 | |||
52 | ''; | ||
53 | }; | ||
54 | }; | ||
55 | } | ||
56 | |||
diff --git a/modules/private/pub/default.nix b/modules/private/pub/default.nix new file mode 100644 index 0000000..c31c8eb --- /dev/null +++ b/modules/private/pub/default.nix | |||
@@ -0,0 +1,52 @@ | |||
1 | { lib, pkgs, config, myconfig, ... }: | ||
2 | { | ||
3 | options = { | ||
4 | myServices.pub.enable = lib.mkOption { | ||
5 | type = lib.types.bool; | ||
6 | default = false; | ||
7 | description = '' | ||
8 | Whether to enable pub user. | ||
9 | ''; | ||
10 | }; | ||
11 | }; | ||
12 | |||
13 | config = lib.mkIf config.myServices.pub.enable { | ||
14 | users.users.pub = let | ||
15 | restrict = pkgs.runCommand "restrict" { | ||
16 | file = ./restrict; | ||
17 | buildInputs = [ pkgs.makeWrapper ]; | ||
18 | } '' | ||
19 | mkdir -p $out/bin | ||
20 | cp $file $out/bin/restrict | ||
21 | chmod a+x $out/bin/restrict | ||
22 | patchShebangs $out/bin/restrict | ||
23 | wrapProgram $out/bin/restrict \ | ||
24 | --prefix PATH : ${lib.makeBinPath [ pkgs.bubblewrap pkgs.rrsync ]} \ | ||
25 | --set TMUX_RESTRICT ${./tmux.restrict.conf} | ||
26 | ''; | ||
27 | purple-hangouts = pkgs.purple-hangouts.overrideAttrs(old: { | ||
28 | installPhase = '' | ||
29 | install -Dm755 -t $out/lib/purple-2/ libhangouts.so | ||
30 | for size in 16 22 24 48; do | ||
31 | install -TDm644 hangouts$size.png $out/share/pixmaps/pidgin/protocols/$size/hangouts.png | ||
32 | done | ||
33 | ''; | ||
34 | }); | ||
35 | in { | ||
36 | createHome = true; | ||
37 | description = "Restricted shell user"; | ||
38 | home = "/var/lib/pub"; | ||
39 | uid = myconfig.env.users.pub.uid; | ||
40 | useDefaultShell = true; | ||
41 | packages = [ | ||
42 | restrict | ||
43 | pkgs.tmux | ||
44 | (pkgs.pidgin.override { plugins = [ | ||
45 | pkgs.purple-plugin-pack purple-hangouts | ||
46 | pkgs.purple-discord pkgs.purple-facebook | ||
47 | pkgs.telegram-purple | ||
48 | ]; }) | ||
49 | ]; | ||
50 | }; | ||
51 | }; | ||
52 | } | ||
diff --git a/modules/private/pub/restrict b/modules/private/pub/restrict new file mode 100644 index 0000000..b2f3be3 --- /dev/null +++ b/modules/private/pub/restrict | |||
@@ -0,0 +1,64 @@ | |||
1 | #!/usr/bin/env bash | ||
2 | user="$1" | ||
3 | rootuser="$HOME/$user/" | ||
4 | mkdir -p $rootuser | ||
5 | |||
6 | orig="$SSH_ORIGINAL_COMMAND" | ||
7 | if [ -z "$orig" ]; then | ||
8 | orig="/bin/bash -l" | ||
9 | fi | ||
10 | if [ "${orig:0:7}" = "command" ]; then | ||
11 | orig="${orig:8}" | ||
12 | fi | ||
13 | |||
14 | case "$orig" in | ||
15 | rsync*) | ||
16 | rrsync $HOME/$user/ | ||
17 | ;; | ||
18 | *) | ||
19 | nix_store_paths() { | ||
20 | nix-store -q -R \ | ||
21 | /run/current-system/sw \ | ||
22 | /etc/profiles/per-user/pub \ | ||
23 | /etc/ssl/certs/ca-bundle.crt \ | ||
24 | | while read i; do | ||
25 | printf '%s--ro-bind\0'$i'\0'$i'\0' '' | ||
26 | done | ||
27 | } | ||
28 | |||
29 | set -euo pipefail | ||
30 | (exec -c bwrap --ro-bind /usr /usr \ | ||
31 | --args 10 \ | ||
32 | --dir /tmp \ | ||
33 | --dir /var \ | ||
34 | --symlink ../tmp var/tmp \ | ||
35 | --proc /proc \ | ||
36 | --dev /dev \ | ||
37 | --ro-bind /etc/resolv.conf /etc/resolv.conf \ | ||
38 | --ro-bind /etc/zoneinfo /etc/zoneinfo \ | ||
39 | --ro-bind /etc/ssl /etc/ssl \ | ||
40 | --ro-bind /etc/static/ssl/certs /etc/static/ssl/certs \ | ||
41 | --ro-bind /run/current-system/sw/lib/locale/locale-archive /etc/locale-archive \ | ||
42 | --ro-bind /run/current-system/sw/bin /bin \ | ||
43 | --ro-bind /etc/profiles/per-user/pub/bin /bin-pub \ | ||
44 | --bind /var/lib/pub/$user /var/lib/pub \ | ||
45 | --dir /var/lib/commons \ | ||
46 | --ro-bind $TMUX_RESTRICT /var/lib/commons/tmux.restrict.conf \ | ||
47 | --chdir /var/lib/pub \ | ||
48 | --unshare-all \ | ||
49 | --share-net \ | ||
50 | --dir /run/user/$(id -u) \ | ||
51 | --setenv TERM "$TERM" \ | ||
52 | --setenv LOCALE_ARCHIVE "/etc/locale-archive" \ | ||
53 | --setenv XDG_RUNTIME_DIR "/run/user/`id -u`" \ | ||
54 | --setenv PS1 "$user@pub $ " \ | ||
55 | --setenv PATH "/bin:/bin-pub" \ | ||
56 | --setenv HOME "/var/lib/pub" \ | ||
57 | --file 11 /etc/passwd \ | ||
58 | --file 12 /etc/group \ | ||
59 | -- $orig) \ | ||
60 | 10< <(nix_store_paths) \ | ||
61 | 11< <(getent passwd $UID 65534) \ | ||
62 | 12< <(getent group $(id -g) 65534) | ||
63 | ;; | ||
64 | esac | ||
diff --git a/modules/private/pub/tmux.restrict.conf b/modules/private/pub/tmux.restrict.conf new file mode 100644 index 0000000..5aefd1c --- /dev/null +++ b/modules/private/pub/tmux.restrict.conf | |||
@@ -0,0 +1,43 @@ | |||
1 | # Pour les nostalgiques de screen | ||
2 | # comme les raccourcis ne sont pas les mêmes, j'évite | ||
3 | set -g prefix C-a | ||
4 | unbind-key C-b | ||
5 | |||
6 | unbind-key -a | ||
7 | bind-key -n C-h list-keys | ||
8 | bind-key C-d detach | ||
9 | bind-key & confirm-before -p "kill-window #W? (y/n)" kill-window | ||
10 | |||
11 | # même hack que sur screen lorsqu'on veut profiter du scroll du terminal | ||
12 | # (xterm ...) | ||
13 | set -g terminal-overrides 'xterm*:smcup@:rmcup@' | ||
14 | |||
15 | #Pour les ctrl+arrow | ||
16 | set-option -g xterm-keys on | ||
17 | |||
18 | # c'est un minimum (defaut 2000) | ||
19 | set-option -g history-limit 10000 | ||
20 | |||
21 | # lorsque j'ai encore un tmux ailleurs seule | ||
22 | # sa fenetre active réduit la taille de ma fenetre locale | ||
23 | setw -g aggressive-resize on | ||
24 | |||
25 | # Pour etre alerté sur un changement dans une autre fenêtre | ||
26 | setw -g monitor-activity on | ||
27 | #set -g visual-activity on | ||
28 | #set -g visual-bell on | ||
29 | |||
30 | set -g base-index 1 | ||
31 | |||
32 | # repercuter le contenu de la fenetre dans la barre de titre | ||
33 | # reference des string : man tmux (status-left) | ||
34 | set -g set-titles on | ||
35 | set -g set-titles-string '#H #W #T' # host window command | ||
36 | |||
37 | #Dans les valeurs par defaut deja, avec le ssh-agent | ||
38 | set -g update-environment "DISPLAY SSH_ASKPASS SSH_AUTH_SOCK SSH_AGENT_PID SSH_CONNECTION WINDOWID XAUTHORITY PATH" | ||
39 | |||
40 | set -g status off | ||
41 | set -g status-left '' | ||
42 | set -g status-right '' | ||
43 | |||
diff --git a/modules/private/ssh/default.nix b/modules/private/ssh/default.nix new file mode 100644 index 0000000..beedaff --- /dev/null +++ b/modules/private/ssh/default.nix | |||
@@ -0,0 +1,40 @@ | |||
1 | { lib, pkgs, config, myconfig, ... }: | ||
2 | { | ||
3 | config = { | ||
4 | networking.firewall.allowedTCPPorts = [ 22 ]; | ||
5 | |||
6 | services.openssh.extraConfig = '' | ||
7 | AuthorizedKeysCommand /etc/ssh/ldap_authorized_keys | ||
8 | AuthorizedKeysCommandUser nobody | ||
9 | ''; | ||
10 | |||
11 | secrets.keys = [{ | ||
12 | dest = "ssh-ldap"; | ||
13 | user = "nobody"; | ||
14 | group = "nogroup"; | ||
15 | permissions = "0400"; | ||
16 | text = myconfig.env.sshd.ldap.password; | ||
17 | }]; | ||
18 | system.activationScripts.sshd = { | ||
19 | deps = [ "secrets" ]; | ||
20 | text = '' | ||
21 | install -Dm400 -o nobody -g nogroup -T /var/secrets/ssh-ldap /etc/ssh/ldap_password | ||
22 | ''; | ||
23 | }; | ||
24 | # ssh is strict about parent directory having correct rights, don't | ||
25 | # move it in the nix store. | ||
26 | environment.etc."ssh/ldap_authorized_keys" = let | ||
27 | ldap_authorized_keys = | ||
28 | pkgs.mylibs.wrap { | ||
29 | name = "ldap_authorized_keys"; | ||
30 | file = ./ldap_authorized_keys.sh; | ||
31 | paths = [ pkgs.which pkgs.gitolite pkgs.openldap pkgs.stdenv.shellPackage pkgs.gnugrep pkgs.gnused pkgs.coreutils ]; | ||
32 | }; | ||
33 | in { | ||
34 | enable = true; | ||
35 | mode = "0755"; | ||
36 | user = "root"; | ||
37 | source = ldap_authorized_keys; | ||
38 | }; | ||
39 | }; | ||
40 | } | ||
diff --git a/modules/private/ssh/ldap_authorized_keys.sh b/modules/private/ssh/ldap_authorized_keys.sh new file mode 100755 index 0000000..d556452 --- /dev/null +++ b/modules/private/ssh/ldap_authorized_keys.sh | |||
@@ -0,0 +1,152 @@ | |||
1 | #!/usr/bin/env bash | ||
2 | |||
3 | LDAPSEARCH=ldapsearch | ||
4 | KEY="immaeSshKey" | ||
5 | LDAP_BIND="cn=ssh,ou=services,dc=immae,dc=eu" | ||
6 | LDAP_PASS=$(cat /etc/ssh/ldap_password) | ||
7 | LDAP_HOST="ldap.immae.eu" | ||
8 | LDAP_MEMBER="cn=users,cn=ssh,ou=services,dc=immae,dc=eu" | ||
9 | LDAP_GITOLITE_MEMBER="cn=users,cn=gitolite,ou=services,dc=immae,dc=eu" | ||
10 | LDAP_PUB_RESTRICT_MEMBER="cn=restrict,cn=pub,ou=services,dc=immae,dc=eu" | ||
11 | LDAP_PUB_FORWARD_MEMBER="cn=forward,cn=pub,ou=services,dc=immae,dc=eu" | ||
12 | LDAP_BASE="dc=immae,dc=eu" | ||
13 | GITOLITE_SHELL=$(which gitolite-shell) | ||
14 | ECHO=$(which echo) | ||
15 | |||
16 | suitable_for() { | ||
17 | type_for="$1" | ||
18 | key="$2" | ||
19 | |||
20 | if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then | ||
21 | echo "$key" | ||
22 | else | ||
23 | key_type=$(cut -d " " -f 1 <<< "$key") | ||
24 | |||
25 | if grep -q "\b-$type_for\b" <<< "$key_type"; then | ||
26 | echo "" | ||
27 | elif grep -q "\b$type_for\b" <<< "$key_type"; then | ||
28 | echo $(sed -e "s/^[^ ]* //g" <<< "$key") | ||
29 | else | ||
30 | echo "" | ||
31 | fi | ||
32 | fi | ||
33 | } | ||
34 | |||
35 | clean_key_line() { | ||
36 | type_for="$1" | ||
37 | line="$2" | ||
38 | |||
39 | if [[ "$line" == $KEY::* ]]; then | ||
40 | # base64 keys should't happen, unless wrong copy-pasting | ||
41 | key="" | ||
42 | else | ||
43 | key=$(sed -e "s/^$KEY: *//" -e "s/ *$//" <<< "$line") | ||
44 | fi | ||
45 | |||
46 | suitable_for "$type_for" "$key" | ||
47 | } | ||
48 | |||
49 | ldap_search() { | ||
50 | $LDAPSEARCH -h $LDAP_HOST -ZZ -b $LDAP_BASE -D $LDAP_BIND -w "$LDAP_PASS" -x -o ldif-wrap=no -LLL "$@" | ||
51 | } | ||
52 | |||
53 | ldap_keys() { | ||
54 | user=$1; | ||
55 | if [[ $user == gitolite ]]; then | ||
56 | ldap_search '(&(memberOf='$LDAP_GITOLITE_MEMBER')('$KEY'=*))' $KEY | \ | ||
57 | while read line ; | ||
58 | do | ||
59 | if [ ! -z "$line" ]; then | ||
60 | if [[ $line == dn* ]]; then | ||
61 | user=$(sed -n 's/.*uid=\([^,]*\).*/\1/p' <<< "$line") | ||
62 | if [ -n "$user" ]; then | ||
63 | if [[ $user == "immae" ]] || [[ $user == "denise" ]]; then | ||
64 | # Capitalize first letter (backward compatibility) | ||
65 | user=$(sed -r 's/^([a-z])/\U\1/' <<< "$user") | ||
66 | fi | ||
67 | else | ||
68 | # Service fake user | ||
69 | user=$(sed -n 's/.*cn=\([^,]*\).*/\1/p' <<< "$line") | ||
70 | fi | ||
71 | elif [[ $line == $KEY* ]]; then | ||
72 | key=$(clean_key_line git "$line") | ||
73 | if [ ! -z "$key" ]; then | ||
74 | if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then | ||
75 | echo -n 'command="'$GITOLITE_SHELL' '$user'",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ' | ||
76 | echo $key | ||
77 | fi | ||
78 | fi | ||
79 | fi | ||
80 | fi | ||
81 | done | ||
82 | exit 0 | ||
83 | elif [[ $user == pub ]]; then | ||
84 | ldap_search '(&(memberOf='$LDAP_PUB_RESTRICT_MEMBER')('$KEY'=*))' $KEY | \ | ||
85 | while read line ; | ||
86 | do | ||
87 | if [ ! -z "$line" ]; then | ||
88 | if [[ $line == dn* ]]; then | ||
89 | echo "" | ||
90 | user=$(sed -n 's/.*uid=\([^,]*\).*/\1/p' <<< "$line") | ||
91 | echo "# $user" | ||
92 | elif [[ $line == $KEY* ]]; then | ||
93 | key=$(clean_key_line pub "$line") | ||
94 | key_forward=$(clean_key_line forward "$line") | ||
95 | if [ ! -z "$key" ]; then | ||
96 | if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then | ||
97 | echo -n 'command="/etc/profiles/per-user/pub/bin/restrict '$user'" ' | ||
98 | echo $key | ||
99 | fi | ||
100 | elif [ ! -z "$key_forward" ]; then | ||
101 | if [[ $key_forward != *$'\n'* ]] && [[ $key_forward == ssh-* ]]; then | ||
102 | echo "# forward only" | ||
103 | echo -n 'no-pty,no-X11-forwarding,command="'$ECHO' forward only" ' | ||
104 | echo $key_forward | ||
105 | fi | ||
106 | fi | ||
107 | fi | ||
108 | fi | ||
109 | done | ||
110 | |||
111 | echo "" | ||
112 | ldap_search '(&(memberOf='$LDAP_PUB_FORWARD_MEMBER')('$KEY'=*))' $KEY | \ | ||
113 | while read line ; | ||
114 | do | ||
115 | if [ ! -z "$line" ]; then | ||
116 | if [[ $line == dn* ]]; then | ||
117 | echo "" | ||
118 | user=$(sed -n 's/.*uid=\([^,]*\).*/\1/p' <<< "$line") | ||
119 | echo "# $user" | ||
120 | elif [[ $line == $KEY* ]]; then | ||
121 | key=$(clean_key_line forward "$line") | ||
122 | if [ ! -z "$key" ]; then | ||
123 | if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then | ||
124 | echo -n 'no-pty,no-X11-forwarding,command="'$ECHO' forward only" ' | ||
125 | echo $key | ||
126 | fi | ||
127 | fi | ||
128 | fi | ||
129 | fi | ||
130 | done | ||
131 | exit 0 | ||
132 | else | ||
133 | ldap_search '(&(memberOf='$LDAP_MEMBER')('$KEY'=*)(uid='$user'))' $KEY | \ | ||
134 | while read line ; | ||
135 | do | ||
136 | if [ ! -z "$line" ]; then | ||
137 | if [[ $line == dn* ]]; then | ||
138 | user=$(sed -n 's/.*uid=\([^,]*\).*/\1/p' <<< "$line") | ||
139 | elif [[ $line == $KEY* ]]; then | ||
140 | key=$(clean_key_line ssh "$line") | ||
141 | if [ ! -z "$key" ]; then | ||
142 | if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then | ||
143 | echo $key | ||
144 | fi | ||
145 | fi | ||
146 | fi | ||
147 | fi | ||
148 | done | ||
149 | fi | ||
150 | } | ||
151 | |||
152 | ldap_keys $@ | ||
diff --git a/modules/private/system.nix b/modules/private/system.nix new file mode 100644 index 0000000..fba504e --- /dev/null +++ b/modules/private/system.nix | |||
@@ -0,0 +1,30 @@ | |||
1 | { pkgs, privateFiles, ... }: | ||
2 | { | ||
3 | config = { | ||
4 | nixpkgs.overlays = builtins.attrValues (import ../../overlays); | ||
5 | _module.args = { | ||
6 | pkgsNext = import <nixpkgsNext> {}; | ||
7 | pkgsPrevious = import <nixpkgsPrevious> {}; | ||
8 | myconfig = { | ||
9 | inherit privateFiles; | ||
10 | env = import "${privateFiles}/environment.nix"; | ||
11 | }; | ||
12 | }; | ||
13 | |||
14 | services.journald.extraConfig = '' | ||
15 | MaxLevelStore="warning" | ||
16 | MaxRetentionSec="1year" | ||
17 | ''; | ||
18 | |||
19 | users.users.root.packages = [ | ||
20 | pkgs.telnet | ||
21 | pkgs.htop | ||
22 | pkgs.iftop | ||
23 | ]; | ||
24 | |||
25 | environment.systemPackages = [ | ||
26 | pkgs.vim | ||
27 | ]; | ||
28 | |||
29 | }; | ||
30 | } | ||
diff --git a/modules/private/tasks/default.nix b/modules/private/tasks/default.nix new file mode 100644 index 0000000..30f49ee --- /dev/null +++ b/modules/private/tasks/default.nix | |||
@@ -0,0 +1,327 @@ | |||
1 | { lib, pkgs, config, myconfig, ... }: | ||
2 | let | ||
3 | cfg = config.myServices.tasks; | ||
4 | server_vardir = config.services.taskserver.dataDir; | ||
5 | fqdn = "task.immae.eu"; | ||
6 | user = config.services.taskserver.user; | ||
7 | env = myconfig.env.tools.task; | ||
8 | group = config.services.taskserver.group; | ||
9 | taskserver-user-certs = pkgs.runCommand "taskserver-user-certs" {} '' | ||
10 | mkdir -p $out/bin | ||
11 | cat > $out/bin/taskserver-user-certs <<"EOF" | ||
12 | #!/usr/bin/env bash | ||
13 | |||
14 | user=$1 | ||
15 | |||
16 | silent_certtool() { | ||
17 | if ! output="$("${pkgs.gnutls.bin}/bin/certtool" "$@" 2>&1)"; then | ||
18 | echo "GNUTLS certtool invocation failed with output:" >&2 | ||
19 | echo "$output" >&2 | ||
20 | fi | ||
21 | } | ||
22 | |||
23 | silent_certtool -p \ | ||
24 | --bits 4096 \ | ||
25 | --outfile "${server_vardir}/userkeys/$user.key.pem" | ||
26 | ${pkgs.gnused}/bin/sed -i -n -e '/^-----BEGIN RSA PRIVATE KEY-----$/,$p' "${server_vardir}/userkeys/$user.key.pem" | ||
27 | |||
28 | silent_certtool -c \ | ||
29 | --template "${pkgs.writeText "taskserver-ca.template" '' | ||
30 | tls_www_client | ||
31 | encryption_key | ||
32 | signing_key | ||
33 | expiration_days = 3650 | ||
34 | ''}" \ | ||
35 | --load-ca-certificate "${server_vardir}/keys/ca.cert" \ | ||
36 | --load-ca-privkey "${server_vardir}/keys/ca.key" \ | ||
37 | --load-privkey "${server_vardir}/userkeys/$user.key.pem" \ | ||
38 | --outfile "${server_vardir}/userkeys/$user.cert.pem" | ||
39 | EOF | ||
40 | chmod a+x $out/bin/taskserver-user-certs | ||
41 | patchShebangs $out/bin/taskserver-user-certs | ||
42 | ''; | ||
43 | taskwarrior-web = pkgs.webapps.taskwarrior-web; | ||
44 | socketsDir = "/run/taskwarrior-web"; | ||
45 | varDir = "/var/lib/taskwarrior-web"; | ||
46 | taskwebPages = let | ||
47 | uidPages = lib.attrsets.zipAttrs ( | ||
48 | lib.lists.flatten | ||
49 | (lib.attrsets.mapAttrsToList (k: c: map (v: { "${v}" = k; }) c.uid) env.taskwarrior-web) | ||
50 | ); | ||
51 | pages = lib.attrsets.mapAttrs (uid: items: | ||
52 | if lib.lists.length items == 1 then | ||
53 | '' | ||
54 | <html> | ||
55 | <head> | ||
56 | <meta http-equiv="refresh" content="0; url=/taskweb/${lib.lists.head items}/" /> | ||
57 | </head> | ||
58 | <body></body> | ||
59 | </html> | ||
60 | '' | ||
61 | else | ||
62 | '' | ||
63 | <html> | ||
64 | <head> | ||
65 | <title>To-do list disponibles</title> | ||
66 | <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | ||
67 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
68 | </head> | ||
69 | <body> | ||
70 | <ul> | ||
71 | ${builtins.concatStringsSep "\n" (map (item: "<li><a href='/taskweb/${item}'>${item}</a></li>") items)} | ||
72 | </ul> | ||
73 | </body> | ||
74 | </html> | ||
75 | '' | ||
76 | ) uidPages; | ||
77 | in | ||
78 | pkgs.runCommand "taskwerver-pages" {} '' | ||
79 | mkdir -p $out/ | ||
80 | ${builtins.concatStringsSep "\n" (lib.attrsets.mapAttrsToList (k: v: "cp ${pkgs.writeText k v} $out/${k}.html") pages)} | ||
81 | echo "Please login" > $out/index.html | ||
82 | ''; | ||
83 | in { | ||
84 | options.myServices.tasks = { | ||
85 | enable = lib.mkEnableOption "my tasks service"; | ||
86 | }; | ||
87 | |||
88 | config = lib.mkIf cfg.enable { | ||
89 | secrets.keys = [{ | ||
90 | dest = "webapps/tools-taskwarrior-web"; | ||
91 | user = "wwwrun"; | ||
92 | group = "wwwrun"; | ||
93 | permissions = "0400"; | ||
94 | text = '' | ||
95 | SetEnv TASKD_HOST "${fqdn}:${toString config.services.taskserver.listenPort}" | ||
96 | SetEnv TASKD_VARDIR "${server_vardir}" | ||
97 | SetEnv TASKD_LDAP_HOST "ldaps://${env.ldap.host}" | ||
98 | SetEnv TASKD_LDAP_DN "${env.ldap.dn}" | ||
99 | SetEnv TASKD_LDAP_PASSWORD "${env.ldap.password}" | ||
100 | SetEnv TASKD_LDAP_BASE "${env.ldap.base}" | ||
101 | SetEnv TASKD_LDAP_FILTER "${env.ldap.search}" | ||
102 | ''; | ||
103 | }]; | ||
104 | services.websites.tools.modules = [ "proxy_fcgi" "sed" ]; | ||
105 | services.websites.tools.vhostConfs.task = { | ||
106 | certName = "eldiron"; | ||
107 | addToCerts = true; | ||
108 | hosts = [ "task.immae.eu" ]; | ||
109 | root = "/run/current-system/webapps/_task"; | ||
110 | extraConfig = [ '' | ||
111 | <Directory /run/current-system/webapps/_task> | ||
112 | DirectoryIndex index.php | ||
113 | Use LDAPConnect | ||
114 | Require ldap-group cn=users,cn=taskwarrior,ou=services,dc=immae,dc=eu | ||
115 | <FilesMatch "\.php$"> | ||
116 | SetHandler "proxy:unix:/var/run/phpfpm/task.sock|fcgi://localhost" | ||
117 | </FilesMatch> | ||
118 | Include /var/secrets/webapps/tools-taskwarrior-web | ||
119 | </Directory> | ||
120 | '' | ||
121 | '' | ||
122 | <Macro Taskwarrior %{folderName}> | ||
123 | ProxyPass "unix://${socketsDir}/%{folderName}.sock|http://localhost-%{folderName}/" | ||
124 | ProxyPassReverse "unix://${socketsDir}/%{folderName}.sock|http://localhost-%{folderName}/" | ||
125 | ProxyPassReverse http://${fqdn}/ | ||
126 | |||
127 | SetOutputFilter Sed | ||
128 | OutputSed "s|/ajax|/taskweb/%{folderName}/ajax|g" | ||
129 | OutputSed "s|\([^x]\)/tasks|\1/taskweb/%{folderName}/tasks|g" | ||
130 | OutputSed "s|\([^x]\)/projects|\1/taskweb/%{folderName}/projects|g" | ||
131 | OutputSed "s|http://${fqdn}/|/taskweb/%{folderName}/|g" | ||
132 | OutputSed "s|/img/relax.jpg|/taskweb/%{folderName}/img/relax.jpg|g" | ||
133 | </Macro> | ||
134 | '' | ||
135 | '' | ||
136 | Alias /taskweb ${taskwebPages} | ||
137 | <Directory "${taskwebPages}"> | ||
138 | DirectoryIndex index.html | ||
139 | Require all granted | ||
140 | </Directory> | ||
141 | |||
142 | RewriteEngine on | ||
143 | RewriteRule ^/taskweb$ /taskweb/ [R=301,L] | ||
144 | RedirectMatch permanent ^/taskweb/([^/]+)$ /taskweb/$1/ | ||
145 | |||
146 | RewriteCond %{LA-U:REMOTE_USER} !="" | ||
147 | RewriteCond ${taskwebPages}/%{LA-U:REMOTE_USER}.html -f | ||
148 | RewriteRule ^/taskweb/?$ ${taskwebPages}/%{LA-U:REMOTE_USER}.html [L] | ||
149 | |||
150 | <Location /taskweb/> | ||
151 | Use LDAPConnect | ||
152 | Require ldap-group cn=users,cn=taskwarrior,ou=services,dc=immae,dc=eu | ||
153 | </Location> | ||
154 | '' | ||
155 | ] ++ (lib.attrsets.mapAttrsToList (k: v: '' | ||
156 | <Location /taskweb/${k}/> | ||
157 | ${builtins.concatStringsSep "\n" (map (uid: "Require ldap-attribute uid=${uid}") v.uid)} | ||
158 | |||
159 | Use Taskwarrior ${k} | ||
160 | </Location> | ||
161 | '') env.taskwarrior-web); | ||
162 | }; | ||
163 | services.phpfpm.poolConfigs = { | ||
164 | tasks = '' | ||
165 | listen = /var/run/phpfpm/task.sock | ||
166 | user = ${user} | ||
167 | group = ${group} | ||
168 | listen.owner = wwwrun | ||
169 | listen.group = wwwrun | ||
170 | pm = dynamic | ||
171 | pm.max_children = 60 | ||
172 | pm.start_servers = 2 | ||
173 | pm.min_spare_servers = 1 | ||
174 | pm.max_spare_servers = 10 | ||
175 | |||
176 | ; Needed to avoid clashes in browser cookies (same domain) | ||
177 | env[PATH] = "/etc/profiles/per-user/${user}/bin" | ||
178 | php_value[session.name] = TaskPHPSESSID | ||
179 | php_admin_value[open_basedir] = "${./www}:/tmp:${server_vardir}:/etc/profiles/per-user/${user}/bin/" | ||
180 | ''; | ||
181 | }; | ||
182 | |||
183 | myServices.websites.webappDirs._task = ./www; | ||
184 | |||
185 | security.acme.certs."task" = config.services.myCertificates.certConfig // { | ||
186 | inherit user group; | ||
187 | plugins = [ "fullchain.pem" "key.pem" "cert.pem" "account_key.json" ]; | ||
188 | domain = fqdn; | ||
189 | postRun = '' | ||
190 | systemctl restart taskserver.service | ||
191 | ''; | ||
192 | }; | ||
193 | |||
194 | users.users.${user}.packages = [ taskserver-user-certs ]; | ||
195 | |||
196 | system.activationScripts.taskserver = { | ||
197 | deps = [ "users" ]; | ||
198 | text = '' | ||
199 | install -m 0750 -o ${user} -g ${group} -d ${server_vardir} | ||
200 | install -m 0750 -o ${user} -g ${group} -d ${server_vardir}/userkeys | ||
201 | install -m 0750 -o ${user} -g ${group} -d ${server_vardir}/keys | ||
202 | |||
203 | if [ ! -e "${server_vardir}/keys/ca.key" ]; then | ||
204 | silent_certtool() { | ||
205 | if ! output="$("${pkgs.gnutls.bin}/bin/certtool" "$@" 2>&1)"; then | ||
206 | echo "GNUTLS certtool invocation failed with output:" >&2 | ||
207 | echo "$output" >&2 | ||
208 | fi | ||
209 | } | ||
210 | |||
211 | silent_certtool -p \ | ||
212 | --bits 4096 \ | ||
213 | --outfile "${server_vardir}/keys/ca.key" | ||
214 | |||
215 | silent_certtool -s \ | ||
216 | --template "${pkgs.writeText "taskserver-ca.template" '' | ||
217 | cn = ${fqdn} | ||
218 | expiration_days = -1 | ||
219 | cert_signing_key | ||
220 | ca | ||
221 | ''}" \ | ||
222 | --load-privkey "${server_vardir}/keys/ca.key" \ | ||
223 | --outfile "${server_vardir}/keys/ca.cert" | ||
224 | |||
225 | chown :${group} "${server_vardir}/keys/ca.key" | ||
226 | chmod g+r "${server_vardir}/keys/ca.key" | ||
227 | fi | ||
228 | ''; | ||
229 | }; | ||
230 | |||
231 | services.taskserver = { | ||
232 | enable = true; | ||
233 | allowedClientIDs = [ "^task [2-9]" "^Mirakel [1-9]" ]; | ||
234 | inherit fqdn; | ||
235 | listenHost = "::"; | ||
236 | pki.manual.ca.cert = "${server_vardir}/keys/ca.cert"; | ||
237 | pki.manual.server.cert = "${config.security.acme.directory}/task/fullchain.pem"; | ||
238 | pki.manual.server.crl = "${config.security.acme.directory}/task/invalid.crl"; | ||
239 | pki.manual.server.key = "${config.security.acme.directory}/task/key.pem"; | ||
240 | requestLimit = 104857600; | ||
241 | }; | ||
242 | |||
243 | system.activationScripts.taskwarrior-web = { | ||
244 | deps = [ "users" ]; | ||
245 | text = '' | ||
246 | if [ ! -f ${server_vardir}/userkeys/taskwarrior-web.cert.pem ]; then | ||
247 | ${taskserver-user-certs}/bin/taskserver-user-certs taskwarrior-web | ||
248 | chown taskd:taskd ${server_vardir}/userkeys/taskwarrior-web.cert.pem ${server_vardir}/userkeys/taskwarrior-web.key.pem | ||
249 | fi | ||
250 | ''; | ||
251 | }; | ||
252 | |||
253 | systemd.services = (lib.attrsets.mapAttrs' (name: userConfig: | ||
254 | let | ||
255 | credentials = "${userConfig.org}/${name}/${userConfig.key}"; | ||
256 | dateFormat = userConfig.date; | ||
257 | taskrc = pkgs.writeText "taskrc" '' | ||
258 | data.location=${varDir}/${name} | ||
259 | taskd.certificate=${server_vardir}/userkeys/taskwarrior-web.cert.pem | ||
260 | taskd.key=${server_vardir}/userkeys/taskwarrior-web.key.pem | ||
261 | # IdenTrust DST Root CA X3 | ||
262 | # obtained here: https://letsencrypt.org/fr/certificates/ | ||
263 | taskd.ca=${pkgs.writeText "ca.cert" '' | ||
264 | -----BEGIN CERTIFICATE----- | ||
265 | MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ | ||
266 | MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT | ||
267 | DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow | ||
268 | PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD | ||
269 | Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB | ||
270 | AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O | ||
271 | rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq | ||
272 | OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b | ||
273 | xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw | ||
274 | 7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD | ||
275 | aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV | ||
276 | HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG | ||
277 | SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 | ||
278 | ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr | ||
279 | AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz | ||
280 | R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 | ||
281 | JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo | ||
282 | Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ | ||
283 | -----END CERTIFICATE-----''} | ||
284 | taskd.server=${fqdn}:${toString config.services.taskserver.listenPort} | ||
285 | taskd.credentials=${credentials} | ||
286 | dateformat=${dateFormat} | ||
287 | ''; | ||
288 | in lib.attrsets.nameValuePair "taskwarrior-web-${name}" { | ||
289 | description = "Taskwarrior webapp for ${name}"; | ||
290 | wantedBy = [ "multi-user.target" ]; | ||
291 | after = [ "network.target" ]; | ||
292 | path = [ pkgs.taskwarrior ]; | ||
293 | |||
294 | environment.TASKRC = taskrc; | ||
295 | environment.BUNDLE_PATH = "${taskwarrior-web.gems}/${taskwarrior-web.gems.ruby.gemPath}"; | ||
296 | environment.BUNDLE_GEMFILE = "${taskwarrior-web.gems.confFiles}/Gemfile"; | ||
297 | environment.LC_ALL = "fr_FR.UTF-8"; | ||
298 | |||
299 | script = '' | ||
300 | exec ${taskwarrior-web.gems}/${taskwarrior-web.gems.ruby.gemPath}/bin/bundle exec thin start -R config.ru -S ${socketsDir}/${name}.sock | ||
301 | ''; | ||
302 | |||
303 | serviceConfig = { | ||
304 | User = user; | ||
305 | PrivateTmp = true; | ||
306 | Restart = "always"; | ||
307 | TimeoutSec = 60; | ||
308 | Type = "simple"; | ||
309 | WorkingDirectory = taskwarrior-web; | ||
310 | StateDirectoryMode = 0750; | ||
311 | StateDirectory = assert lib.strings.hasPrefix "/var/lib/" varDir; | ||
312 | (lib.strings.removePrefix "/var/lib/" varDir + "/${name}"); | ||
313 | RuntimeDirectoryPreserve = "yes"; | ||
314 | RuntimeDirectory = assert lib.strings.hasPrefix "/run/" socketsDir; | ||
315 | lib.strings.removePrefix "/run/" socketsDir; | ||
316 | }; | ||
317 | |||
318 | unitConfig.RequiresMountsFor = varDir; | ||
319 | }) env.taskwarrior-web) // { | ||
320 | taskserver-ca.postStart = '' | ||
321 | chown :${group} "${server_vardir}/keys/ca.key" | ||
322 | chmod g+r "${server_vardir}/keys/ca.key" | ||
323 | ''; | ||
324 | }; | ||
325 | |||
326 | }; | ||
327 | } | ||
diff --git a/modules/private/tasks/www/index.php b/modules/private/tasks/www/index.php new file mode 100644 index 0000000..deaf8af --- /dev/null +++ b/modules/private/tasks/www/index.php | |||
@@ -0,0 +1,157 @@ | |||
1 | <?php | ||
2 | if (!isset($_SERVER["REMOTE_USER"])) { | ||
3 | die("please login"); | ||
4 | } | ||
5 | $ldap_user = $_SERVER["REMOTE_USER"]; | ||
6 | $ldap_host = getenv("TASKD_LDAP_HOST"); | ||
7 | $ldap_dn = getenv('TASKD_LDAP_DN'); | ||
8 | $ldap_password = getenv('TASKD_LDAP_PASSWORD'); | ||
9 | $ldap_base = getenv('TASKD_LDAP_BASE'); | ||
10 | $ldap_filter = getenv('TASKD_LDAP_FILTER'); | ||
11 | $host = getenv('TASKD_HOST'); | ||
12 | $vardir = getenv('TASKD_VARDIR'); | ||
13 | |||
14 | $connect = ldap_connect($ldap_host); | ||
15 | ldap_set_option($connect, LDAP_OPT_PROTOCOL_VERSION, 3); | ||
16 | if (!$connect || !ldap_bind($connect, $ldap_dn, $ldap_password)) { | ||
17 | die("impossible to connect to LDAP"); | ||
18 | } | ||
19 | |||
20 | $search_query = str_replace('%login%', ldap_escape($ldap_user), $ldap_filter); | ||
21 | |||
22 | $search = ldap_search($connect, $ldap_base, $search_query); | ||
23 | $info = ldap_get_entries($connect, $search); | ||
24 | |||
25 | if (ldap_count_entries($connect, $search) != 1) { | ||
26 | die("Impossible to find user in LDAP"); | ||
27 | } | ||
28 | |||
29 | $entries = []; | ||
30 | foreach($info[0]["immaetaskid"] as $key => $value) { | ||
31 | if ($key !== "count") { | ||
32 | $entries[] = explode(":", $value); | ||
33 | } | ||
34 | } | ||
35 | |||
36 | if (isset($_GET["file"])) { | ||
37 | $basecert = $vardir . "/userkeys/" . $ldap_user; | ||
38 | if (!file_exists($basecert . ".cert.pem")) { | ||
39 | exec("taskserver-user-certs $ldap_user"); | ||
40 | } | ||
41 | $certificate = file_get_contents($basecert . ".cert.pem"); | ||
42 | $cert_key = file_get_contents($basecert . ".key.pem"); | ||
43 | |||
44 | // IdenTrust DST Root CA X3 | ||
45 | // obtained here: https://letsencrypt.org/fr/certificates/ | ||
46 | $server_cert = "-----BEGIN CERTIFICATE----- | ||
47 | MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ | ||
48 | MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT | ||
49 | DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow | ||
50 | PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD | ||
51 | Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB | ||
52 | AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O | ||
53 | rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq | ||
54 | OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b | ||
55 | xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw | ||
56 | 7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD | ||
57 | aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV | ||
58 | HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG | ||
59 | SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 | ||
60 | ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr | ||
61 | AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz | ||
62 | R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 | ||
63 | JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo | ||
64 | Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ | ||
65 | -----END CERTIFICATE-----"; | ||
66 | |||
67 | $file = $_GET["file"]; | ||
68 | switch($file) { | ||
69 | case "ca.cert.pem": | ||
70 | $content = $server_cert; | ||
71 | $name = "ca.cert.pem"; | ||
72 | $type = "application/x-x509-ca-cert"; | ||
73 | break; | ||
74 | case "cert.pem": | ||
75 | $content = $certificate; | ||
76 | $name = $ldap_user . ".cert.pem"; | ||
77 | $type = "application/x-x509-ca-cert"; | ||
78 | break; | ||
79 | case "key.pem": | ||
80 | $content = $cert_key; | ||
81 | $name = $ldap_user . ".key.pem"; | ||
82 | $type = "application/x-x509-ca-cert"; | ||
83 | break; | ||
84 | case "mirakel"; | ||
85 | foreach ($entries as $entry) { | ||
86 | list($org, $user, $key) = $entry; | ||
87 | if ($key == $_GET["key"]) { break; } | ||
88 | } | ||
89 | $name = $user . ".mirakel"; | ||
90 | $type = "text/plain"; | ||
91 | $content = "username: $user | ||
92 | org: $org | ||
93 | user key: $key | ||
94 | server: $host | ||
95 | client.cert: | ||
96 | $certificate | ||
97 | Client.key: | ||
98 | $cert_key | ||
99 | ca.cert: | ||
100 | $server_cert | ||
101 | "; | ||
102 | break; | ||
103 | default: | ||
104 | die("invalid file name"); | ||
105 | break; | ||
106 | } | ||
107 | |||
108 | header("Content-Type: $type"); | ||
109 | header('Content-Disposition: attachment; filename="' . $name . '"'); | ||
110 | header('Content-Transfer-Encoding: binary'); | ||
111 | header('Accept-Ranges: bytes'); | ||
112 | header('Cache-Control: private'); | ||
113 | header('Pragma: private'); | ||
114 | echo $content; | ||
115 | exit; | ||
116 | } | ||
117 | ?> | ||
118 | <html> | ||
119 | <header> | ||
120 | <title>Taskwarrior configuration</title> | ||
121 | </header> | ||
122 | <body> | ||
123 | <ul> | ||
124 | <li><a href="?file=ca.cert.pem">ca.cert.pem</a></li> | ||
125 | <li><a href="?file=cert.pem"><?php echo $ldap_user; ?>.cert.pem</a></li> | ||
126 | <li><a href="?file=key.pem"><?php echo $ldap_user; ?>.key.pem</a></li> | ||
127 | </ul> | ||
128 | For command line interface, download the files, put them near your Taskwarrior | ||
129 | configuration files, and add that to your Taskwarrior configuration: | ||
130 | <pre> | ||
131 | taskd.certificate=/path/to/<?php echo $ldap_user; ?>.cert.pem | ||
132 | taskd.key=/path/to/<?php echo $ldap_user; ?>.key.pem | ||
133 | taskd.server=<?php echo $host ."\n"; ?> | ||
134 | <?php if (count($entries) > 1) { | ||
135 | echo "# Chose one of them\n"; | ||
136 | foreach($entries as $entry) { | ||
137 | list($org, $user, $key) = $entry; | ||
138 | echo "# taskd.credentials=$org/$user/$key\n"; | ||
139 | } | ||
140 | } else { ?> | ||
141 | taskd.credentials=<?php echo $entries[0][0]; ?>/<?php echo $entries[0][1]; ?>/<?php echo $entries[0][2]; ?> | ||
142 | <?php } ?> | ||
143 | taskd.ca=/path/to/ca.cert.pem | ||
144 | </pre> | ||
145 | For Mirakel, download and import the file: | ||
146 | <ul> | ||
147 | <?php | ||
148 | foreach ($entries as $entry) { | ||
149 | list($org, $user, $key) = $entry; | ||
150 | echo '<li><a href="?file=mirakel&key='.$key.'">' . $user . '.mirakel</a></li>'; | ||
151 | } | ||
152 | ?> | ||
153 | </ul> | ||
154 | For Android Taskwarrior app, see instructions <a href="https://bitbucket.org/kvorobyev/taskwarriorandroid/wiki/Configuration">here</a>. | ||
155 | </body> | ||
156 | </html> | ||
157 | |||
diff --git a/modules/private/websites/tools/git/default.nix b/modules/private/websites/tools/git/default.nix index 3e8b605..75d0240 100644 --- a/modules/private/websites/tools/git/default.nix +++ b/modules/private/websites/tools/git/default.nix | |||
@@ -4,7 +4,9 @@ let | |||
4 | inherit (pkgs.webapps) mantisbt_2 mantisbt_2-plugins; | 4 | inherit (pkgs.webapps) mantisbt_2 mantisbt_2-plugins; |
5 | env = myconfig.env.tools.mantisbt; | 5 | env = myconfig.env.tools.mantisbt; |
6 | }; | 6 | }; |
7 | gitweb = pkgs.callPackage ./gitweb.nix { gitoliteDir = config.services.myGitolite.gitoliteDir; }; | 7 | gitweb = pkgs.callPackage ./gitweb.nix { |
8 | gitoliteDir = config.myServices.gitolite.gitoliteDir; | ||
9 | }; | ||
8 | 10 | ||
9 | cfg = config.myServices.websites.tools.git; | 11 | cfg = config.myServices.websites.tools.git; |
10 | in { | 12 | in { |