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