Server executing client's requested configuration with the right permissions
[hcoop/domtool2.git] / src / domain.sml
CommitLineData
a3698041
AC
1(* HCoop Domtool (http://hcoop.sourceforge.net/)
2 * Copyright (c) 2006, Adam Chlipala
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
dac62e84 17 *)
a3698041
AC
18
19(* Domain-related primitive actions *)
20
21structure Domain :> DOMAIN = struct
22
2ed6d0e5 23structure SM = DataStructures.StringMap
12adf55a 24structure SS = DataStructures.StringSet
2ed6d0e5 25
12adf55a 26val nodes = map #1 Config.nodeIps
2ed6d0e5
AC
27val nodeMap = foldl (fn ((node, ip), mp) => SM.insert (mp, node, ip))
28 SM.empty Config.nodeIps
8a7c40fa 29fun nodeIp node = valOf (SM.find (nodeMap, node))
2ed6d0e5 30
12adf55a 31val usr = ref ""
12adf55a
AC
32fun getUser () = !usr
33
34val your_doms = ref SS.empty
35fun your_domains () = !your_doms
36
8a7c40fa
AC
37val your_usrs = ref SS.empty
38fun your_users () = !your_usrs
39
40val your_grps = ref SS.empty
41fun your_groups () = !your_grps
42
43val your_pths = ref SS.empty
44fun your_paths () = !your_pths
45
aa56e112
AC
46fun setUser user =
47 (usr := Config.testUser;
48 your_doms := Acl.class {user = getUser (),
49 class = "domain"};
50 your_usrs := Acl.class {user = getUser (),
51 class = "user"};
52 your_grps := Acl.class {user = getUser (),
53 class = "group"};
54 your_pths := Acl.class {user = getUser (),
55 class = "path"})
56
6ae327f8
AC
57fun validIp s =
58 case map Int.fromString (String.fields (fn ch => ch = #".") s) of
59 [SOME n1, SOME n2, SOME n3, SOME n4] =>
60 n1 >= 0 andalso n1 < 256 andalso n2 >= 0 andalso n2 < 256 andalso n3 >= 0 andalso n3 < 256 andalso n4 >= 0 andalso n4 < 256
61 | _ => false
62
629a34f6
AC
63fun isIdent ch = Char.isLower ch orelse Char.isDigit ch
64
65fun validHost s =
66 size s > 0 andalso size s < 20
67 andalso CharVector.all (fn ch => isIdent ch orelse ch = #"-") s
68
69fun validDomain s =
70 size s > 0 andalso size s < 100
71 andalso List.all validHost (String.fields (fn ch => ch = #".") s)
72
2ed6d0e5 73fun validNode s = List.exists (fn s' => s = s') nodes
e0b0abd2 74
12adf55a 75fun yourDomain s = SS.member (your_domains (), s)
8a7c40fa
AC
76fun yourUser s = SS.member (your_users (), s)
77fun yourGroup s = SS.member (your_groups (), s)
78fun yourPath path =
79 List.all (fn s => s <> "..") (String.fields (fn ch => ch = #"/") path)
80 andalso CharVector.all (fn ch => Char.isAlphaNum ch orelse ch = #"." orelse ch = #"/"
81 orelse ch = #"-" orelse ch = #"_") path
82 andalso SS.exists (fn s' => path = s' orelse String.isPrefix (s' ^ "/") path) (your_paths ())
12adf55a 83
edd38024 84fun yourDomainHost s =
c98b57cf
AC
85 yourDomain s
86 orelse let
edd38024
AC
87 val (pref, suf) = Substring.splitl (fn ch => ch <> #".") (Substring.full s)
88 in
edd38024
AC
89 Substring.size suf > 0
90 andalso validHost (Substring.string pref)
91 andalso yourDomain (Substring.string
c98b57cf 92 (Substring.slice (suf, 1, NONE)))
edd38024
AC
93 end
94
2aeb9eec
AC
95fun validUser s = size s > 0 andalso size s < 20
96 andalso CharVector.all Char.isAlphaNum s
97
98val validGroup = validUser
99
f8dfbbcc
AC
100val _ = Env.type_one "no_spaces"
101 Env.string
102 (CharVector.all (fn ch => not (Char.isSpace ch)))
d5754b53
AC
103val _ = Env.type_one "no_newlines"
104 Env.string
105 (CharVector.all (fn ch => ch <> #"\n" andalso ch <> #"\r"))
f8dfbbcc 106
6ae327f8
AC
107val _ = Env.type_one "ip"
108 Env.string
109 validIp
110
629a34f6
AC
111val _ = Env.type_one "host"
112 Env.string
113 validHost
114
115val _ = Env.type_one "domain"
116 Env.string
117 validDomain
118
12adf55a
AC
119val _ = Env.type_one "your_domain"
120 Env.string
121 yourDomain
122
edd38024
AC
123val _ = Env.type_one "your_domain_host"
124 Env.string
125 yourDomainHost
126
2aeb9eec
AC
127val _ = Env.type_one "user"
128 Env.string
129 validUser
130
131val _ = Env.type_one "group"
132 Env.string
133 validGroup
134
8a7c40fa
AC
135val _ = Env.type_one "your_user"
136 Env.string
137 yourUser
138
139val _ = Env.type_one "your_group"
140 Env.string
141 yourGroup
142
143val _ = Env.type_one "your_path"
144 Env.string
145 yourPath
146
e0b0abd2
AC
147val _ = Env.type_one "node"
148 Env.string
149 validNode
150
a3698041
AC
151open Ast
152
6ae327f8
AC
153val dl = ErrorMsg.dummyLoc
154
155val nsD = (EString Config.defaultNs, dl)
156val serialD = (EVar "serialAuto", dl)
157val refD = (EInt Config.defaultRefresh, dl)
158val retD = (EInt Config.defaultRetry, dl)
159val expD = (EInt Config.defaultExpiry, dl)
160val minD = (EInt Config.defaultMinimum, dl)
161
162val soaD = multiApp ((EVar "soa", dl),
163 dl,
164 [nsD, serialD, refD, retD, expD, minD])
165
e0b0abd2
AC
166val masterD = (EApp ((EVar "internalMaster", dl),
167 (EString Config.defaultNode, dl)),
168 dl)
169
aa56e112
AC
170val _ = Defaults.registerDefault ("DNS",
171 (TBase "dnsKind", dl),
172 (fn () => multiApp ((EVar "useDns", dl),
173 dl,
174 [soaD, masterD, (EList [], dl)])))
6ae327f8 175
aa56e112
AC
176val _ = Defaults.registerDefault ("TTL",
177 (TBase "int", dl),
178 (fn () => (EInt Config.Bind.defaultTTL, dl)))
6ae327f8
AC
179
180type soa = {ns : string,
181 serial : int option,
182 ref : int,
183 ret : int,
184 exp : int,
185 min : int}
186
187val serial = fn (EVar "serialAuto", _) => SOME NONE
188 | (EApp ((EVar "serialConst", _), n), _) => Option.map SOME (Env.int n)
189 | _ => NONE
190
191val soa = fn (EApp ((EApp ((EApp ((EApp ((EApp ((EApp
192 ((EVar "soa", _), ns), _),
193 sl), _),
194 rf), _),
195 ret), _),
196 exp), _),
197 min), _) =>
198 (case (Env.string ns, serial sl, Env.int rf,
199 Env.int ret, Env.int exp, Env.int min) of
200 (SOME ns, SOME sl, SOME rf,
201 SOME ret, SOME exp, SOME min) =>
202 SOME {ns = ns,
203 serial = sl,
204 ref = rf,
205 ret = ret,
206 exp = exp,
207 min = min}
208 | _ => NONE)
209 | _ => NONE
210
e0b0abd2
AC
211datatype master =
212 ExternalMaster of string
213 | InternalMaster of string
214
215val master = fn (EApp ((EVar "externalMaster", _), e), _) => Option.map ExternalMaster (Env.string e)
216 | (EApp ((EVar "internalMaster", _), e), _) => Option.map InternalMaster (Env.string e)
217 | _ => NONE
218
6ae327f8 219datatype dnsKind =
e0b0abd2
AC
220 UseDns of {soa : soa,
221 master : master,
222 slaves : string list}
6ae327f8
AC
223 | NoDns
224
e0b0abd2
AC
225val dnsKind = fn (EApp ((EApp ((EApp
226 ((EVar "useDns", _), sa), _),
227 mstr), _),
228 slaves), _) =>
229 (case (soa sa, master mstr, Env.list Env.string slaves) of
230 (SOME sa, SOME mstr, SOME slaves) =>
231 SOME (UseDns {soa = sa,
232 master = mstr,
233 slaves = slaves})
234 | _ => NONE)
325285ab 235 | (EVar "noDns", _) => SOME NoDns
6ae327f8
AC
236 | _ => NONE
237
a3698041
AC
238val befores = ref (fn (_ : string) => ())
239val afters = ref (fn (_ : string) => ())
240
241fun registerBefore f =
242 let
243 val old = !befores
244 in
245 befores := (fn x => (old x; f x))
246 end
247
248fun registerAfter f =
249 let
250 val old = !afters
251 in
252 afters := (fn x => (old x; f x))
253 end
254
255val current = ref ""
e0b0abd2 256val currentPath = ref (fn (_ : string) => "")
dac62e84 257
d612d62c
AC
258val scratch = ref ""
259
dac62e84
AC
260fun currentDomain () = !current
261
e0b0abd2
AC
262fun domainFile {node, name} = ((*print ("Opening " ^ !currentPath node ^ name ^ "\n");*)
263 TextIO.openOut (!currentPath node ^ name))
dac62e84
AC
264
265fun getPath domain =
266 let
267 val toks = String.fields (fn ch => ch = #".") domain
268
269 val elems = foldr (fn (piece, elems) =>
270 let
271 val elems = piece :: elems
d612d62c 272
e0b0abd2
AC
273 fun doNode node =
274 let
275 val path = String.concatWith "/"
276 (Config.resultRoot :: node :: rev elems)
277 val tmpPath = String.concatWith "/"
278 (Config.tmpDir :: node :: rev elems)
279 in
280 (if Posix.FileSys.ST.isDir
281 (Posix.FileSys.stat path) then
282 ()
283 else
284 (OS.FileSys.remove path;
285 OS.FileSys.mkDir path))
286 handle OS.SysErr _ => OS.FileSys.mkDir path;
287
288 (if Posix.FileSys.ST.isDir
289 (Posix.FileSys.stat tmpPath) then
290 ()
291 else
292 (OS.FileSys.remove tmpPath;
293 OS.FileSys.mkDir tmpPath))
294 handle OS.SysErr _ => OS.FileSys.mkDir tmpPath
295 end
296 in
2ed6d0e5 297 app doNode nodes;
dac62e84
AC
298 elems
299 end) [] toks
300 in
e0b0abd2 301 fn (root, site) => String.concatWith "/" (root :: site :: rev ("" :: elems))
d612d62c
AC
302 end
303
304datatype file_action' =
305 Add' of {src : string, dst : string}
306 | Delete' of string
307 | Modify' of {src : string, dst : string}
308
e0b0abd2 309fun findDiffs (site, dom, acts) =
d612d62c 310 let
e0b0abd2
AC
311 val gp = getPath dom
312 val realPath = gp (Config.resultRoot, site)
313 val tmpPath = gp (Config.tmpDir, site)
314
315 (*val _ = print ("getDiffs(" ^ site ^ ", " ^ dom ^ ")... " ^ realPath ^ "; " ^ tmpPath ^ "\n")*)
d612d62c
AC
316
317 val dir = Posix.FileSys.opendir realPath
318
319 fun loopReal acts =
320 case Posix.FileSys.readdir dir of
321 NONE => (Posix.FileSys.closedir dir;
322 acts)
323 | SOME fname =>
324 let
325 val real = OS.Path.joinDirFile {dir = realPath,
326 file = fname}
327 val tmp = OS.Path.joinDirFile {dir = tmpPath,
328 file = fname}
329 in
330 if Posix.FileSys.ST.isDir (Posix.FileSys.stat real) then
331 loopReal acts
332 else if Posix.FileSys.access (tmp, []) then
8df2e702 333 if Slave.shell [Config.diff, " ", real, " ", tmp] then
d612d62c
AC
334 loopReal acts
335 else
e0b0abd2 336 loopReal ((site, dom, realPath, Modify' {src = tmp, dst = real}) :: acts)
d612d62c 337 else
e0b0abd2 338 loopReal ((site, dom, realPath, Delete' real) :: acts)
d612d62c
AC
339 end
340
e0b0abd2 341 val acts = loopReal acts
d612d62c 342
8df2e702 343 val dir = Posix.FileSys.opendir tmpPath
d612d62c
AC
344
345 fun loopTmp acts =
346 case Posix.FileSys.readdir dir of
347 NONE => (Posix.FileSys.closedir dir;
348 acts)
349 | SOME fname =>
350 let
351 val real = OS.Path.joinDirFile {dir = realPath,
352 file = fname}
353 val tmp = OS.Path.joinDirFile {dir = tmpPath,
354 file = fname}
355 in
356 if Posix.FileSys.ST.isDir (Posix.FileSys.stat tmp) then
357 loopTmp acts
358 else if Posix.FileSys.access (real, []) then
359 loopTmp acts
360 else
e0b0abd2 361 loopTmp ((site, dom, realPath, Add' {src = tmp, dst = real}) :: acts)
d612d62c
AC
362 end
363
364 val acts = loopTmp acts
365 in
366 acts
dac62e84 367 end
a3698041 368
e0b0abd2
AC
369fun findAllDiffs () =
370 let
371 val dir = Posix.FileSys.opendir Config.tmpDir
372 val len = length (String.fields (fn ch => ch = #"/") Config.tmpDir) + 1
373
374 fun exploreSites diffs =
375 case Posix.FileSys.readdir dir of
376 NONE => diffs
377 | SOME site =>
378 let
379 fun explore (dname, diffs) =
380 let
381 val dir = Posix.FileSys.opendir dname
382
383 fun loop diffs =
384 case Posix.FileSys.readdir dir of
385 NONE => diffs
386 | SOME name =>
387 let
388 val fname = OS.Path.joinDirFile {dir = dname,
389 file = name}
390 in
391 loop (if Posix.FileSys.ST.isDir (Posix.FileSys.stat fname) then
392 let
393 val dom = String.fields (fn ch => ch = #"/") fname
394 val dom = List.drop (dom, len)
395 val dom = String.concatWith "." (rev dom)
396
397 val dname' = OS.Path.joinDirFile {dir = dname,
398 file = name}
399 in
400 explore (dname',
401 findDiffs (site, dom, diffs))
402 end
403 else
404 diffs)
405 end
406 in
407 loop diffs
408 before Posix.FileSys.closedir dir
409 end
410 in
411 explore (OS.Path.joinDirFile {dir = Config.tmpDir,
412 file = site}, diffs)
413 end
414 in
415 exploreSites []
416 before Posix.FileSys.closedir dir
417 end
418
419val masterNode : string option ref = ref NONE
420fun dnsMaster () = !masterNode
421
6ae327f8
AC
422val _ = Env.containerV_one "domain"
423 ("domain", Env.string)
424 (fn (evs, dom) =>
425 let
426 val kind = Env.env dnsKind (evs, "DNS")
427 val ttl = Env.env Env.int (evs, "TTL")
428
e0b0abd2 429 val path = getPath dom
6ae327f8
AC
430
431 val () = (current := dom;
e0b0abd2 432 currentPath := (fn site => path (Config.tmpDir, site)))
6ae327f8 433
e0b0abd2 434 fun saveSoa (kind, soa : soa) node =
6ae327f8 435 let
e0b0abd2 436 val outf = domainFile {node = node, name = "soa"}
6ae327f8
AC
437 in
438 TextIO.output (outf, kind);
439 TextIO.output (outf, "\n");
440 TextIO.output (outf, Int.toString ttl);
441 TextIO.output (outf, "\n");
442 TextIO.output (outf, #ns soa);
443 TextIO.output (outf, "\n");
444 case #serial soa of
445 NONE => ()
446 | SOME n => TextIO.output (outf, Int.toString n);
447 TextIO.output (outf, "\n");
448 TextIO.output (outf, Int.toString (#ref soa));
449 TextIO.output (outf, "\n");
450 TextIO.output (outf, Int.toString (#ret soa));
451 TextIO.output (outf, "\n");
452 TextIO.output (outf, Int.toString (#exp soa));
453 TextIO.output (outf, "\n");
454 TextIO.output (outf, Int.toString (#min soa));
455 TextIO.output (outf, "\n");
456 TextIO.closeOut outf
457 end
458
2ed6d0e5 459 fun saveNamed (kind, soa : soa, masterIp) node =
6ae327f8 460 let
e0b0abd2 461 val outf = domainFile {node = node, name = "named.conf"}
6ae327f8
AC
462 in
463 TextIO.output (outf, "\nzone \"");
464 TextIO.output (outf, dom);
465 TextIO.output (outf, "\" IN {\n\ttype ");
466 TextIO.output (outf, kind);
467 TextIO.output (outf, ";\n\tfile \"");
468 TextIO.output (outf, Config.Bind.zonePath);
469 TextIO.output (outf, "/");
470 TextIO.output (outf, dom);
471 TextIO.output (outf, ".zone\";\n");
472 case kind of
473 "master" => TextIO.output (outf, "\tallow-update { none; };\n")
2ed6d0e5
AC
474 | _ => (TextIO.output (outf, "\tmasters { ");
475 TextIO.output (outf, masterIp);
476 TextIO.output (outf, " };\n"));
477 TextIO.output (outf, "};\n");
6ae327f8
AC
478 TextIO.closeOut outf
479 end
6ae327f8
AC
480 in
481 case kind of
e0b0abd2
AC
482 NoDns => masterNode := NONE
483 | UseDns dns =>
2ed6d0e5
AC
484 let
485 val masterIp =
486 case #master dns of
487 InternalMaster node => valOf (SM.find (nodeMap, node))
488 | ExternalMaster ip => ip
489 in
490 app (saveSoa ("slave", #soa dns)) (#slaves dns);
491 app (saveNamed ("slave", #soa dns, masterIp)) (#slaves dns);
492 case #master dns of
493 InternalMaster node =>
494 (masterNode := SOME node;
495 saveSoa ("master", #soa dns) node;
496 saveNamed ("master", #soa dns, masterIp) node)
497 | _ => masterNode := NONE;
498 !befores dom
499 end
6ae327f8 500 end,
e0b0abd2
AC
501 fn () => !afters (!current))
502
503val () = Env.registerPre (fn () => (ignore (Slave.shellF ([Config.rm, " -rf ", Config.tmpDir, ""],
504 fn cl => "Temp file cleanup failed: " ^ cl));
505 OS.FileSys.mkDir Config.tmpDir;
506 app (fn node => OS.FileSys.mkDir
507 (OS.Path.joinDirFile {dir = Config.tmpDir,
508 file = node}))
2ed6d0e5 509 nodes;
e0b0abd2
AC
510 app (fn node => OS.FileSys.mkDir
511 (OS.Path.joinDirFile {dir = Config.resultRoot,
512 file = node})
513 handle OS.SysErr _ => ())
2ed6d0e5 514 nodes))
e0b0abd2
AC
515
516val () = Env.registerPost (fn () =>
517 let
518 val diffs = findAllDiffs ()
6ae327f8 519
e0b0abd2
AC
520 val diffs = map (fn (site, dom, dir, Add' {src, dst}) =>
521 (Slave.shellF ([Config.cp, " ", src, " ", dst],
522 fn cl => "Copy failed: " ^ cl);
523 (site,
6ae327f8
AC
524 {action = Slave.Add,
525 domain = dom,
526 dir = dir,
e0b0abd2
AC
527 file = dst}))
528 | (site, dom, dir, Delete' dst) =>
529 (OS.FileSys.remove dst
530 handle OS.SysErr _ =>
531 ErrorMsg.error NONE ("Delete failed for " ^ dst);
532 (site,
6ae327f8
AC
533 {action = Slave.Delete,
534 domain = dom,
535 dir = dir,
e0b0abd2
AC
536 file = dst}))
537 | (site, dom, dir, Modify' {src, dst}) =>
538 (Slave.shellF ([Config.cp, " ", src, " ", dst],
539 fn cl => "Copy failed: " ^ cl);
540 (site,
6ae327f8
AC
541 {action = Slave.Modify,
542 domain = dom,
543 dir = dir,
e0b0abd2
AC
544 file = dst}))) diffs
545 in
546 if !ErrorMsg.anyErrors then
547 ()
548 else
559e89e9 549 Slave.handleChanges (map #2 diffs);
e0b0abd2 550 ignore (Slave.shellF ([Config.rm, " -rf ", Config.tmpDir, ""],
559e89e9 551 fn cl => "Temp file cleanup failed: " ^ cl))
e0b0abd2 552 end)
6ae327f8
AC
553
554
a3698041
AC
555
556end