1#!%TCLSH%
2
3#
4# Network graph building daemon
5#
6# When a modification is done on an equipment (either by a human or by
7# a program when a user is using web interface to change characteristics
8# of an interface), this modification is detected by:
9# - detectconfmod script which reads syslog output
10# - radius daemon
11# They both write an entry in topo.modeq table of the database.
12#
13# The topographd daemon exploit this topo.modeq table. Its algorithm
14# is as follows.
15#
16#	Infinite loop
17#	    once by night, fetches all equipment configuration files
18#		(full rancid), analyzes every configuration file and
19#		rebuild the graph.
20#
21#	    when an equipment specification is modified in topo.eq
22#		table, generates the router.db rancid file), fetches
23#		all equipments (full rancid), analyzes all configuration
24#		files and rebuild the graph.
25#
26#	    when a vlan specification is modified in topo.vlan table,
27#		generates the vlan.eq file, and rebuild the graph.
28#
29#	    when an equipment is modified (topo.modeq table), fetches
30#		its configuration, analyzes it, and rebuild the graph.
31#	    	When graph is rebuilt, mark the entry in topo.modeq as
32#		"processed".
33#
34# History
35#  2010/02/16 : pda/jean : design
36#  2010/12/18 : pda      : rework installation
37#  2012/01/18 : pda      : ranciddb -> ranciddir
38#  2012/03/27 : pda/jean : daemonization
39#
40
41set conf(extractcoll)	{extractcoll -a -s -w -i -p}
42set conf(start-rancid)	{start-rancid %1$s}
43set conf(anaconf)	{anaconf}
44
45source %LIBNETMAGIS%
46
47##############################################################################
48# Graph updating
49##############################################################################
50
51#
52# Detect separator in file using the following heuristic:
53# the expected format of the file is:
54#
55# <alpha-numeric characters or dot>+<Separator>
56# Separator is the first non-alphanumeric and non-dot character
57# (ie. a hostname or fully qualified domain name)
58# found after the first field in the file.
59#
60# Input:
61#   - filename : file name to open
62# Output:
63#   - return value : character used as separator (":", ";" etc.),
64#   - msg: error message if
65#
66# History
67#   2015/09/08 : jean/boggia : design
68#
69#
70
71proc detect-separator {filename _msg} {
72    upvar $_msg msg
73
74    set separator ""
75    set msg ""
76    if { [catch {open $filename "r"} fd] } then {
77        set msg "detect-separator: cannot open $filename: $fd"
78    } else {
79        if {[gets $fd line] >= 0} then {
80            if {![regexp {^[.a-zA-Z0-9-]+([^.a-zA-Z0-9])} $line dummy separator]} then {
81                set msg "detect-separator: separator not found in $filename"
82            }
83        } else {
84            set msg "detect-separator: could not read first line of $filename"
85        }
86    }
87    return $separator
88}
89
90# Generate a new router.db rancid file
91#
92# Input:
93#   - none
94# Output:
95#   - return value : empty string or error message
96#
97# History
98#   2010/12/13 : pda/jean : design
99#   2012/04/25 : pda      : create file if it doesn't exist (even if no modif)
100#
101
102proc update-routerdb {} {
103    global ctxt
104
105    set sql "SELECT * FROM topo.modeq
106		    WHERE eq = '_routerdb' AND processed = 0"
107    set found 0
108    if {! [toposqlselect $sql tab { set found 1 }]} then {
109	return "Cannot read equipment modification from database"
110    }
111
112    if {$found || ! [file exists $ctxt(routerdb)]} then {
113	set sql "SELECT e.eq, t.type, e.up
114			FROM topo.eq e, topo.eqtype t
115			WHERE e.idtype = t.idtype"
116	set leq {}
117	if {! [toposqlselect $sql t { lappend leq [list $t(eq) $t(type) $t(up)] }]} then {
118	    return "Cannot read equipment list from database"
119	}
120
121	set msg ""
122	set sep [detect-separator $ctxt(routerdb) msg]
123        if {$msg ne ""} then {
124	    return $msg
125        }
126	set new "$ctxt(routerdb).new"
127	if {[catch {set fd [open $new "w"]} msg]} then {
128	    return "Cannot create $new ($msg)"
129	}
130	foreach e $leq {
131	    lassign $e eq type up
132	    if {$up} then { set up "up" } else { set up "down" }
133	    puts $fd "$eq$sep$type$sep$up"
134	}
135	if {[catch {close $fd} msg]} then {
136	    return "Cannot close $new ($msg)"
137	}
138	if {[catch {file rename -force $new $ctxt(routerdb)} msg]} then {
139	    return "Cannot move $new to $ctxt(routerdb) ($msg)"
140	}
141	set sql "UPDATE topo.modeq SET processed = 1 WHERE eq = '_routerdb'"
142	if {! [toposqlexec $sql]} then {
143	    return "Cannot update equipment modification for _routerdb"
144	}
145    }
146    return ""
147}
148
149#
150# Guess if a full rancid run is needed
151#
152# Input:
153#   - routerdbmod : in return, result from detect-filemod
154# Output:
155#   - return value :
156#	-1 : error
157#	0 : no full rancid needed
158#	1 : full rancid needed
159#   - parameter routerdbmod : detect-filemod result
160#
161# History
162#   2010/10/15 : pda/jean : design
163#
164
165proc full-rancid-needed {_routerdbmod} {
166    global ctxt
167    upvar $_routerdbmod routerdbmod
168
169    set msg [update-routerdb]
170    if {$msg ne ""} then {
171	keep-state-mail "router.db" $msg
172	return -1
173    }
174
175    set sql "SELECT topo.lastrun.date IS NULL
176		    OR (
177			(date_trunc('day',topo.lastrun.date)
178			    <> date_trunc('day',now())
179			AND extract(hour from now())>=$ctxt(fullrancidmin)
180			AND extract(hour from now())<=$ctxt(fullrancidmax))
181		    )
182	       AS result
183	       FROM topo.lastrun"
184
185    # if selects succeeds, returns the result of SQL query,
186    # while translating it to 1 (true) or 0 (false)
187    set r2 1
188    set r [toposqlselect $sql tab { set r2 [expr $tab(result) ? 1 : 0]}]
189    if {$r} then {
190	set r $r2
191
192	# detect if router.db has been modified
193	set routerdbmod {}
194	set fmod [detect-filemod $ctxt(routerdb)]
195	if {[llength $fmod] > 0} then {
196	    lassign $fmod code path date
197	    switch $code {
198		err {
199		    set msg $date
200		    set r -1
201		}
202		add {
203		    set msg "File router.db added"
204		    set r 1
205		}
206		mod {
207		    set msg "Resuming normal operation"
208		    set r 1
209		}
210		del {
211		    set msg "File router.db deleted"
212		    set r -1
213		}
214	    }
215	    keep-state-mail "router.db" $msg
216	    if {$r == 1} then {
217		set routerdbmod $fmod
218	    }
219	} else {
220	    keep-state-mail "router.db" "Resuming normal operation"
221	}
222    }
223
224    return $r
225}
226
227#
228# Update topo graph from equipment configuration files
229#
230# Input:
231#   - full : 1 if a full rancid is needed
232#   - routerdbmod : result from detect-filemod, or empty
233#   - leq : list of modified equipments (may be with a fake "_vlan" equipement)
234#   - leqvirt : modified virtual equipments, whose date must be reset in
235#	database. See detect-dirmod for the format of this list.
236# Output:
237#   - return value : 1 if ok, 0 if error
238#
239# History
240#   2010/10/15 : pda/jean : design
241#   2010/10/20 : pda/jean : coding
242#   2010/11/12 : pda/jean : add leqvirt
243#
244
245proc update-graph {full routerdbmod leq leqvirt} {
246    global conf
247
248    #
249    # Reset equipments marked as modified in the topo.modeq table
250    #
251
252    if {! [toposqllock]} then {
253	return 0
254    }
255
256    if {[llength $leq] == 0} then {
257	set sql "UPDATE topo.modeq SET processed = 1"
258    } else {
259	set inlist [join $leq "', '"]
260	set sql "UPDATE topo.modeq SET processed = 1 WHERE eq IN ('$inlist')"
261    }
262    if {! [toposqlexec $sql]} then {
263	return 0
264    }
265
266    #
267    # Run rancid and send a mail if needed
268    #
269
270    if {$full} then {
271	set callrancid 1
272	set leqrancid {}
273    } else {
274	# It is not a full rancid run.
275	# Remove dummy equipment "_vlan". If, after this removal, there
276	# is no equipment, distinguish from the "full-rancid" case.
277        set pos [lsearch -exact $leq "_vlan"]
278        if {$pos != -1} then {
279            set leqrancid [lreplace $leq $pos $pos]
280        } else {
281            set leqrancid $leq
282        }
283	if {[llength $leqrancid] == 0} then {
284	    set callrancid 0
285	} else {
286	    set callrancid 1
287	}
288    }
289
290    if {$callrancid} then {
291	if {! [rancid $leqrancid]} then {
292	    toposqlunlock "abort"
293	    return 0
294	}
295    }
296
297    #
298    # Update modification time of router.db if needed
299    #
300
301    if {[llength $routerdbmod] > 0} then {
302	if {! [sync-filemonitor [list $routerdbmod]]} then {
303	    toposqlunlock "abort"
304	    return 0
305	}
306    }
307
308    # If it is not a "full anaconf" run, add virtual equipments
309
310    if {$full} then {
311	set leqanaconf {}
312    } else {
313	set leqanaconf $leq
314	foreach meq $leqvirt {
315	    topo-verbositer "processing $meq" 9
316	    lassign $meq code path date
317	    if {$code eq "add" || $code eq "mod"} then {
318		if {[regexp {([^/]+)\.eq$} $path bidon eq]} then {
319		    topo-verbositer "adding virtual $eq to leqanaconf" 9
320		    lappend leqanaconf $eq
321		}
322	    }
323	}
324    }
325
326    #
327    # Update graph and send a mail if needed
328    #
329
330    if {! [anaconf $leqanaconf]} then {
331	toposqlunlock "abort"
332	return 0
333    }
334
335    #
336    # Update modification time of virtual equipments
337    #
338
339    if {! [sync-filemonitor $leqvirt]} then {
340	toposqlunlock "abort"
341	return 0
342    }
343
344    #
345    # Update sensor list
346    #
347
348    if {! [sensors]} then {
349	toposqlunlock "abort"
350	return 0
351    }
352
353    #
354    # Update date of last full rancid/anaconf
355    #
356
357    if {[llength $leq] == 0} then {
358	set sql "DELETE FROM topo.lastrun ;
359		    INSERT INTO topo.lastrun (date) VALUES (NOW ())"
360	if {! [toposqlexec $sql]} then {
361	    return 0
362	}
363    }
364    toposqlunlock "commit"
365
366    return 1
367}
368
369#
370# Call rancid
371#
372# Input:
373#   - leq (optional) : modified equipment list
374# Output:
375#   - return value : 1 if ok, 0 if error
376#
377# History
378#   2010/10/20 : pda/jean : design
379#
380
381proc rancid {{leq {}}} {
382    global conf
383    global ctxt
384
385    if {[llength $leq] == 0} then {
386	set-status "Ranciding all equipements"
387    } else {
388	set-status "Ranciding $leq"
389    }
390
391    #
392    # Call rancid
393    #
394
395    set cmd [format "$ctxt(topobindir)/$conf(start-rancid)" $leq]
396    topo-verbositer "rancid : cmd=<$cmd>" 2
397
398    if {[catch {exec sh -c $cmd} msg]} then {
399	# erreur
400	set msg "Error while running '$cmd'\n$msg"
401	set r 0
402    } else {
403	# No error: msg contains rancid output
404	if {$msg eq ""} then {
405	    set msg "Resuming normal operation"
406	}
407	set r 1
408    }
409
410    #
411    # Send a mail if needed
412    #
413
414    if {[llength $leq] == 0} then {
415	set ev "fullrancid"
416    } else {
417	set ev "rancid"
418    }
419
420    keep-state-mail $ev $msg
421
422    return $r
423}
424
425#
426# Call anaconf to build the graph
427#
428# Input:
429#   - leq (optional) : modified equipment list
430# Output:
431#   - return value : 1 if ok, 0 if error
432#
433# History
434#   2010/10/20 : pda/jean : design
435#
436
437proc anaconf {{leq {}}} {
438    global conf
439    global ctxt
440
441    if {[llength $leq] == 0} then {
442	set-status "Building graph for all equipements"
443    } else {
444	set-status "Building graph for $leq"
445    }
446
447    set text ""
448
449    set cmd "$ctxt(topobindir)/$conf(anaconf)"
450
451    set r 1
452    foreach eq $leq {
453	append cmd " $eq"
454    }
455
456    topo-verbositer "anaconf : cmd=<$cmd>" 2
457    if {[catch {exec sh -c $cmd} msg]} then {
458	set msg "Error in $cmd\n$msg"
459	set r 0
460    } else {
461	# no error
462    }
463    set text $msg
464
465    #
466    # Send a mail if needed
467    #
468
469    keep-state-mail "anaconf" $msg
470
471    return $r
472}
473
474#
475# Read sensor list and update it in database
476#
477# Input: none
478# Output:
479#   - return value : 1 if ok, 0 if error
480#
481# History
482#   2010/11/09 : pda/jean : design
483#
484
485proc sensors {} {
486    global conf
487    global ctxt
488
489    set-status "Updating sensor list"
490
491    #
492    # Read existing sensors in database
493    #
494
495    set sql "SELECT * FROM topo.sensor"
496    set r [toposqlselect $sql tab {
497				set id $tab(id)
498				set told($id) [list $tab(type) $tab(eq) \
499					$tab(comm) $tab(iface) $tab(param)]
500			    } ]
501    if {! $r} then {
502	keep-state-mail "sensors" "Cannot read sensor list from database"
503	return 0
504    }
505
506    #
507    # Red new sensor list from the graph
508    #
509
510    if {! [read-coll tnew msg]} then {
511	keep-state-mail "sensors" "Cannot read sensor list from graph\n$msg"
512	return 0
513    }
514    if {$msg ne ""} then {
515	keep-state-mail "sensors" "Inconsistent sensors:\n$msg\nSensor list not updated."
516	return 1
517    }
518
519    #
520    # Difference analysis
521    #
522
523    set lunmod {}
524    set sql {}
525
526    foreach id [array names tnew] {
527	lassign $tnew($id) type eq comm iface param
528	set qtype [::pgsql::quote $type]
529	set qid [::pgsql::quote $id]
530	set qeq [::pgsql::quote $eq]
531	set qcomm [::pgsql::quote $comm]
532	set qiface [::pgsql::quote $iface]
533	set qparam [::pgsql::quote $param]
534
535	if {[info exists told($id)]} then {
536	    #
537	    # Update common sensors
538	    #
539
540	    if {$tnew($id) eq $told($id)} then {
541		#
542		# Same : just update lastseen date
543		#
544		lappend lunmod "'$qid'"
545	    } else {
546		#
547		# Not the same: update all fields
548		#
549		lappend sql "UPDATE topo.sensor
550				    SET type = '$qtype',
551					eq = '$qeq',
552					comm = '$qcomm',
553					iface = '$qiface',
554					param = '$qparam',
555					lastmod = DEFAULT,
556					lastseen = DEFAULT
557				    WHERE id = '$qid'"
558	    }
559
560	    unset told($id)
561	} else {
562	    #
563	    # New sensor
564	    #
565	    lappend sql \
566		"INSERT INTO topo.sensor (id, type, eq, comm, iface, param)
567		    VALUES ('$qid','$qtype','$qeq','$qcomm','$qiface','$qparam')"
568	}
569    }
570
571    #
572    # Update date of sensors seen, but not modified
573    #
574
575    if {[llength $lunmod] > 0} then {
576	set l [join $lunmod ","]
577	lappend sql "UPDATE topo.sensor SET lastseen = DEFAULT WHERE id IN ($l)"
578    }
579
580    #
581    # Remove old sensors, after some delay
582    #
583
584    lappend sql "DELETE FROM topo.sensor
585		    WHERE lastseen + interval '$ctxt(sensorexpire) days' < now()"
586
587    #
588    # Send the huuuuuge SQL command
589    #
590
591    if {[llength $sql] > 0} then {
592	set sql [join $sql ";"]
593	if {! [toposqlexec $sql]} then {
594	    keep-state-mail "sensors" "Cannot write sensors in database"
595	    return 0
596	}
597    }
598
599    #
600    # Send a mail if needed
601    #
602
603    keep-state-mail "sensors" "Resuming normal operation"
604
605    return 1
606}
607
608#
609# Read lines from "extractcoll" and get sensor list
610#
611# Input:
612#   - _tab : in return, information extracted
613#   - _msg : in return, empty string or error/warning message
614# Output:
615#   - return value : 1 if ok, 0 if error
616#   - parameter _tab: array, indexed by sensor names, containing a list
617#	{<type> <eq> <community> [<iface> [<param>]]}
618#
619# Note :
620#   Format of input file is:
621#	trafic      <id coll> <eq> <community> <phys iface> <vlan|->
622#	nbassocwifi <id coll> <eq> <community> <phys iface> <ssid>
623#	nbauthwifi  <id coll> <eq> <community> <phys iface> <ssid>
624#	port        <id coll> <eq> <community> <eqtype> <vlan id> {<iflist...>}
625#	ipmac       <id coll> <eq> <community> <eqtype>
626#
627# History
628#   2008/07/28 : pda/boggia : design
629#   2008/07/30 : pda        : adapt to new input format
630#   2010/11/09 : pda/jean   : topographd integration
631#   2010/12/19 : pda        : use call-topo
632#   2010/12/21 : pda        : any message is not automatically an error
633#   2011/12/08 : jean       : portmac and ipmac collector integration
634#
635
636proc read-coll {_tab _msg} {
637    global conf
638    upvar $_tab tab
639    upvar $_msg msg
640
641    set cmd $conf(extractcoll)
642    if {! [call-topo $cmd msg]} then {
643	return 0
644    }
645
646    set lwarn {}
647    foreach line [split $msg "\n"] {
648	set l [split $line]
649	switch [lindex $l 0] {
650	    trafic {
651		lassign $l kw id eq comm iface vlan
652		if {$vlan ne "-"} then {
653		    set iface "$iface.$vlan"
654		}
655		set sensor [list $kw $eq $comm $iface {}]
656	    }
657	    nbassocwifi -
658	    nbauthwifi {
659		lassign $l kw id eq comm iface ssid
660		set sensor [list $kw $eq $comm $iface $ssid]
661	    }
662	    portmac {
663		lassign $l kw id eq comm eqtype ifacelist vlanid
664		set sensor [list "portmac.$eqtype" $eq $comm $ifacelist $vlanid]
665	    }
666	    ipmac {
667		lassign $l kw id eq comm eqtype
668		set sensor [list "ipmac" $eq $comm {} {}]
669	    }
670	    default {
671		lappend lwarn "Unknown sensor type ($l)"
672		set sensor ""
673	    }
674	}
675
676	if {$sensor ne ""} then {
677	    if {[info exists tab($id)]} then {
678		# same format for all sensor types, until now
679		lassign $tab($id) okw oeq ocomm oiface
680		lappend lwarn "Sensor '$id' seen more than once ($eq/$iface and $oeq/$oiface)"
681	    }
682	    set tab($id) $sensor
683	}
684    }
685
686    set msg [join $lwarn "\n"]
687    return 1
688}
689
690##############################################################################
691# Detection of equipment modified
692##############################################################################
693
694#
695# Detect modifications on equipments
696#
697# Output:
698#   - return value : list of modified equipments, or empty list
699#
700# History
701#   2010/10/21 : pda/jean : design
702#   2011/01/05 : jean : known equipements are pulled from rancid
703#
704
705proc detect-mod {} {
706
707    set l {}
708    set sql "SELECT DISTINCT(eq) AS eq FROM topo.modeq WHERE processed = 0"
709    if {! [toposqlselect $sql tab { lappend l $tab(eq) }]} then {
710	return {}
711    }
712
713    set sql "SELECT eq FROM topo.eq WHERE up=1"
714    if {! [toposqlselect $sql tab {set rancideq($tab(eq)) 1}]} then {
715	return {}
716    }
717
718    #
719    # Check that equipment is managed by rancid
720    # Note: according to equipment types, syslogd versions, and equipment
721    # local configuration, names may be short names (not fqdn). In those
722    # cases, we suppose that equipment is not managed (it is surely
723    # an error in the detection script)
724    #
725
726    set leq {}
727    set lunk {}
728    foreach eq $l {
729	if {[info exists rancideq($eq)]} then {
730	    lappend leq $eq
731	} elseif {$eq eq "_vlan"} then {
732	    lappend leq $eq
733	} elseif {$eq ne "_routerdb"} then {
734	    lappend lunk $eq
735	}
736    }
737
738    if {[llength $lunk] == 0} then {
739	keep-state-mail "detectunknw" "Resuming normal operation"
740    } else {
741	keep-state-mail "detectunknw" \
742			"Change detected on unknown equipments ($lunk)"
743    }
744
745    return $leq
746}
747
748##############################################################################
749# Main program
750##############################################################################
751
752# The -z option is reserved for internal use
753set usage {usage: %1$s [-h][-f][-v <n>]
754    -h         : display this text
755    -f         : run in foreground
756    -v <n>     : verbose level (0 = none, 1 = minimum, 99 = max)
757}
758
759proc usage {argv0} {
760    global usage
761
762    puts stderr [format $usage $argv0]
763}
764
765#
766# Main program
767#
768
769proc main {argv0 argv} {
770    global conf
771    global ctxt
772
773    set ctxt(dbfd1) ""
774    set ctxt(dbfd2) ""
775    set verbose 0
776    set foreground 0
777    set daemonized 0
778
779    #
780    # Get configuration values from local file
781    #
782
783    set-log [get-local-conf "logger"]
784
785    set ctxt(routerdb) [get-local-conf "ranciddir"]
786    append ctxt(routerdb) "/router.db"
787
788    set eqvirtdir [get-local-conf "eqvirtdir"]
789    set ctxt(topobindir) [get-local-conf "topobindir"]
790
791    #
792    # Get configuration values from database
793    #
794
795    config ::dnsconfig
796    lazy-connect
797
798    set delay [dnsconfig get "topographddelay"]
799    set delay [expr $delay*1000]
800
801    set ctxt(maxstatus) [dnsconfig get "topomaxstatus"]
802
803    set ctxt(sensorexpire) [dnsconfig get "sensorexpire"]
804    set ctxt(modeqexpire) [dnsconfig get "modeqexpire"]
805
806    set ctxt(fullrancidmin) [dnsconfig get "fullrancidmin"]
807    set ctxt(fullrancidmax) [dnsconfig get "fullrancidmax"]
808
809    #
810    # Argument analysis
811    #
812
813    while {[llength $argv] > 0} {
814	switch -glob -- [lindex $argv 0] {
815	    -h {
816		usage $argv0
817		return 0
818	    }
819	    -f {
820		set foreground 1
821		set argv [lreplace $argv 0 0]
822	    }
823	    -z {
824	    	# This option is not meant to be used by a human
825		# It implies that the program is being rerun in order to be
826		# daemonized
827		set daemonized 1
828		set argv [lreplace $argv 0 0]
829	    }
830	    -v {
831		set verbose [lindex $argv 1]
832		set argv [lreplace $argv 0 1]
833
834	    }
835	    -* {
836		usage $argv0
837		return 1
838	    }
839	    default {
840		break
841	    }
842	}
843    }
844
845    if {[llength $argv] != 0} then {
846	usage $argv0
847	return 1
848    }
849
850    if {! $foreground && ! $daemonized} then {
851    	set argstr {}
852	if {$verbose > 0} then {
853	    lappend argstr -v $verbose
854	}
855	lappend argstr "-z"
856	run-as-daemon $argv0 [join $argstr " "]
857    }
858
859    reset-status
860    set-status "Starting topographd"
861
862    #
863    # Default values
864    #
865
866    topo-set-verbose $verbose
867
868    if {$verbose > 0} then {
869	set-trace {toposqlselect toposqlexec toposqllock toposqlunlock
870		    keep-state-mail
871		    update-routerdb full-rancid-needed update-graph
872		    rancid anaconf sensors read-coll read-eq-type
873		    detect-mod
874		    detect-filemod detect-dirmod sync-filemonitor}
875
876    }
877
878    #
879    # Daemon main loop
880    #
881
882    set first 1
883
884    while {true} {
885	#
886	# Except first time, wait for the delay
887	#
888
889	topo-verbositer "delay : first=$first delay=$delay" 10
890	if {! $first} then {
891	    after $delay
892	}
893	set first 0
894
895	#
896	# Detect if a full rancid is needed (i.e. if no full rancid
897	# has been done since 2 o'clock this morning, for example).
898	#
899
900	switch [full-rancid-needed routerdbmod] {
901	    -1 {
902		# error
903		continue
904	    }
905	    0 {
906		# not needed
907	    }
908	    1 {
909		# graph must be updated
910		# check modification times of virtual equipments
911		# in order to update them after configuration read.
912		set leqvirt [detect-dirmod $eqvirtdir err]
913		if {$err ne ""} then {
914		    keep-state-mail "eqvirt" $err
915		    continue
916		}
917
918		if {! [update-graph 1 $routerdbmod {} $leqvirt]} then {
919		    continue
920		}
921	    }
922	}
923
924	#
925	# Search for modified equipments and rebuild the graph
926	#
927
928	# virtual equipments
929	set leqvirt [detect-dirmod $eqvirtdir err]
930	if {$err ne ""} then {
931	    keep-state-mail "eqvirt" $err
932	    continue
933	}
934
935	set leq [detect-mod]
936	if {[llength $leq] > 0 || [llength $leqvirt] > 0} then {
937	    update-graph 0 {} $leq $leqvirt
938	}
939    }
940}
941
942exit [main $argv0 $argv]
943