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