1#! %TCLSH% 2 3# 4# Daemon to send interface modifications on equipments 5# 6# When a user ask, via the Netmagis interface, to change an interface 7# on an equipment (VLAN, VoIP VLAN or description), the CGI script 8# writes an entry in topo.ifchange table. 9# This entry contains: 10# - user's login 11# - date of change request 12# - equipment (resource record id) 13# - interface 14# - description 15# - vlan 16# - voip vlan 17# When the change is processed by this daemon, following attributes 18# are updated: 19# - modification date 20# - modification log (output from modification command) 21# 22# To process an interface change, this daemon performs the following 23# tasks: 24# - test if equipment is reachable, via fping, without waiting 25# for the TCP timeout 26# - send appropriate commands on equipment (which, in turns, will 27# trigger a modification detection by the topographd daemon) 28# - if no problem was detected, mark the entry as "processed" 29# Notice: error detection is very primitive, since rancid 30# can just detect fatal error (equipment unreachable, etc.) 31# and not tell if commands were successfull. So, it is important 32# to consult log. 33# 34# History 35# 2010/02/16 : pda/jean : design 36# 2010/10/15 : pda/jean : splitted in a different daemon 37# 2010/12/19 : pda : rework installation 38# 2012/03/27 : pda/jean : daemonization 39# 40 41set conf(extracteq) {extracteq -a %1$s %2$s} 42 43source %LIBNETMAGIS% 44 45 46############################################################################## 47# Send changes to equipments 48############################################################################## 49 50# 51# Send changes specified in topo.ifchange table to equipements 52# 53# Input: 54# - _tabeq : array containing equipment types and models 55# Output: 56# - return value : 1 if ok, 0 if error 57# 58# History : 59# 2010/10/14 : pda/jean : design 60# 61 62proc send-changes {_tabeq} { 63 upvar $_tabeq tabeq 64 65 # 66 # Find modification requests and build a list {fqdn fqdn...} 67 # 68 69 set leq {} 70 set sql "SELECT DISTINCT (eq) AS fqdn 71 FROM topo.ifchanges 72 WHERE processed = 0" 73 if {! [toposqlselect $sql tab { lappend leq $tab(fqdn) }]} then { 74 return 0 75 } 76 77 foreach eq $leq { 78 # 79 # Search equipment type and model from fqdn 80 # 81 82 if {! [info exists tabeq($eq)]} then { 83 update-modlog $eq "Unknown equipement type for '$eq'" 84 continue 85 } 86 lassign $tabeq($eq) type model 87 88 # 89 # Reachability test 90 # 91 92 set msg [test-ping $eq] 93 if {$msg ne ""} then { 94 update-modlog $eq $msg 95 continue 96 } 97 98 # 99 # Equipment is alive. Get all unprocessed modifications for 100 # this equipment, translate them in commands, send these 101 # commands and mark these modifications as "processed" 102 # 103 104 set lcmd [mod-to-conf $eq $type $model lreqdate] 105 if {[llength $lcmd] == 0} then { 106 continue 107 } 108 109 if {[execute-cmd $eq $type $model $lcmd msg]} then { 110 mark-processed $eq $lreqdate $msg 111 } else { 112 update-modlog $eq $msg 113 } 114 } 115 116 return 1 117} 118 119# 120# Translate modification requests into commands for this equipment 121# 122# Input: 123# - eq : equipment fqdn 124# - type : equipment type (ex: cisco, juniper...) 125# - model : equipment model (ex: 3750, M20...) 126# - lreqdate : in return, list of modification requests (dates) 127# Output: 128# - return value : list of command lines to send to equipment or 129# empty list if an error occurred 130# - parameter lreqdate : see above 131# 132# History : 133# 2010/10/14 : pda/jean : design 134# 135 136proc mod-to-conf {eq type model _lreqdate} { 137 upvar $_lreqdate lreqdate 138 139 set lreqdate {} 140 set lcmd {} 141 142 # 143 # Prologue 144 # 145 146 set prologue [fetch-cmd $type $model "prologue"] 147 set lcmd [concat $lcmd $prologue] 148 149 # 150 # Extract all modification requests for this equipment 151 # 152 153 set qeq [::pgsql::quote $eq] 154 set sql "SELECT * 155 FROM topo.ifchanges 156 WHERE eq = '$qeq' AND processed = 0 157 ORDER BY reqdate ASC" 158 159 set l {} 160 if {! [toposqlselect $sql tab { lappend l [list $tab(reqdate) $tab(iface) \ 161 $tab(ifdesc) $tab(ethervlan) $tab(voicevlan)] }]} then { 162 return {} 163 } 164 165 foreach e $l { 166 lassign $e reqdate iface ifdesc ethervlan voicevlan 167 168 lappend lreqdate $reqdate 169 170 # 171 # Interface description 172 # 173 174 if {$ifdesc ne ""} then { 175 foreach fmt [fetch-cmd $type $model "ifdesc"] { 176 lappend lcmd [format $fmt $iface $ifdesc] 177 } 178 } 179 180 # 181 # Reset interface in a known state 182 # 183 184 set lcmd [concat $lcmd [resetif $eq $type $model $iface]] 185 186 # 187 # Ether Voice Operation 188 # ----------------------------------------------------- 189 # -1 -1 Shutdown interface 190 # -1 > 0 Voice vlan only 191 # > 0 -1 Access vlan only 192 # > 0 > 0 Access vlan + voice vlan 193 # 194 195 set ether [expr $ethervlan==-1] 196 set voice [expr $voicevlan==-1] 197 198 switch -- "$ether$voice" { 199 11 { 200 foreach fmt [fetch-cmd $type $model "ifdisable"] { 201 lappend lcmd [format $fmt $iface] 202 } 203 } 204 10 { 205 foreach fmt [fetch-cmd $type $model "ifvoice"] { 206 lappend lcmd [format $fmt $iface $voicevlan] 207 } 208 } 209 01 { 210 foreach fmt [fetch-cmd $type $model "ifaccess"] { 211 lappend lcmd [format $fmt $iface $ethervlan] 212 } 213 } 214 00 { 215 foreach fmt [fetch-cmd $type $model "ifvoice"] { 216 lappend lcmd [format $fmt $iface $voicevlan] 217 } 218 foreach fmt [fetch-cmd $type $model "ifaccess"] { 219 lappend lcmd [format $fmt $iface $ethervlan] 220 } 221 } 222 } 223 } 224 225 # 226 # Epilogue 227 # 228 229 set epilogue [fetch-cmd $type $model "epilogue"] 230 set lcmd [concat $lcmd $epilogue] 231 232 return $lcmd 233} 234 235# 236# Load shell commands use to send configuration commands to equipments 237# 238# Input: 239# - none 240# Output: 241# - return value: 1 if send command are valid in ctxt array, 0 if error 242# - global ctxt(send...): shell commands for each equipment type 243# - global ctxt(send): 1 if send commands are already in ctxt(send...) 244# 245# History : 246# 2012/01/25 : pda/jean : design 247# 248 249proc get-send {} { 250 global ctxt 251 252 if {! $ctxt(send)} then { 253 set lt {} 254 set sql "SELECT type FROM topo.eqtype" 255 if {[toposqlselect $sql tab { lappend lt $tab(type) }]} then { 256 foreach t $lt { 257 set ctxt(send$t) [get-local-conf "send$t"] 258 } 259 set ctxt(send) 1 260 } 261 } 262 263 return $ctxt(send) 264} 265 266# 267# Send commands on an equipment 268# 269# Input: 270# - fqdn : equipment fqdn 271# - type : equipment type (ex: cisco, juniper...) 272# - model : equipment model (ex: 3750, M20...) 273# - lcmd : list of commands to send 274# - _msg : in return, error message or command output 275# Output 276# - return value : 1 if ok, 0 if error 277# 278# History : 279# 2010/02/18 : pda/jean : design 280# 281 282proc execute-cmd {fqdn type model lcmd _msg} { 283 upvar $_msg msg 284 global ctxt 285 286 set-status "Sending command to $fqdn" 287 288 set tmp "/tmp/topod.[pid]" 289 set fd [open $tmp "w"] 290 puts $fd [join $lcmd "\n"] 291 close $fd 292 293 if {![get-send]} then { 294 set msg "Unable to read send* commands" 295 return 0 296 } 297 298 if {[info exists ctxt(send$type)]} then { 299 set exec $ctxt(send$type) 300 } elseif {$ctxt($send$type) eq ""} then { 301 set msg "Directive 'send$type' not configured in netmagis.conf" 302 return 0 303 } else { 304 set msg "Unknown equipment type '$type'" 305 return 0 306 } 307 308 if {[catch {exec sh -c "$exec -x $tmp $fqdn"} msg]} then { 309 set r 0 310 } else { 311 set r 1 312 } 313 314 file delete -force $tmp 315 316 # 317 # Interpret output file 318 # 319 # Ideas: 320 # 1- analyze file by removing all known lines: 321 # (ex: "cisco(Fa1/0)# switchport blablabla" -> remove) 322 # all remaining lines are error lines 323 # -> pb : this strategy is not very sustainable 324 # 2- look for error patterns 325 # -> pb : number of unknown errors is not countable 326 # 3- ignore output file and detect modifications not in 327 # the rebuilt graph 328 # -> pb : delay between modification and check 329 # -> pb : program to check would be very complex 330 # 4- ignore output file and let people detect problems 331 # (eg: display output file in a diagnostic page or in 332 # the Netmagis equipment page) 333 # 334 # We choose the 4th idea at this time. Experience will say if 335 # it is a good idea or no. 336 # 337 338 return $r 339} 340 341# 342# Return the appropriate command for an equipment type 343# 344# Input: 345# - type : equipment type (ex: cisco, juniper...) 346# - model : equipment model (ex: 3750, M20, ...) 347# - context : action selection (ex: ifaccess, ifenable, ...) 348# Output: 349# - return value : list of commands to execute, or empty list if not found 350# 351# Note: 352# The confcmd table contains commands adapted to each equipment type. 353# 354# model may be a regular expression (.*, .*29.0.* etc.) 355# The lowest ranked regexp matched for model is kept. 356# 357# "command" is a list of command lines to send to the equipment. 358# 359# Different actions are: 360# 361# prologue 362# enter in configuration mode 363# ifreset 364# reset interface to a known state (most of the time, by removing 365# all vlans) 366# Parameters : 367# %1$ : interface name 368# resetvlan 369# when an interface cannot be reset in a known state (for example 370# on HP switches), this is the command to execute for each vlan 371# to remove it. 372# Parameters : 373# %1$ : interface name 374# %2$ : vlan-id 375# ifaccess 376# set an access vlan on an interface 377# Parameters : 378# %1$ : interface name 379# %2$ : vlan-id 380# ifvoice 381# set a voip vlan on an interface 382# Parameters : 383# %1$ : interface name 384# %2$ : vlan-id 385# ifdesc 386# set interface description 387# Parameters : 388# %1$ : interface name 389# %2$ : description 390# epilogue 391# exit configuration mode an commit modification 392# 393# History : 394# 2010/02/16 : pda/jean : design 395# 2012/01/25 : pda/jean : adaptation to database 396# 397 398proc fetch-cmd {type model context} { 399 set qtype [::pgsql::quote $type] 400 set qcontext [::pgsql::quote $context] 401 set sql "SELECT c.model, c.command 402 FROM topo.confcmd c, topo.eqtype e 403 WHERE c.idtype = e.idtype 404 AND e.type = '$qtype' 405 AND c.action = '$qcontext' 406 ORDER BY c.rank ASC 407 " 408 set l {} 409 if {! [toposqlselect $sql tab { lappend l [list $tab(model) $tab(command)] }]} then { 410 return {} 411 } 412 413 set r {} 414 foreach elem $l { 415 lassign $elem remodel command 416 if {[regexp "^$remodel\$" $model]} then { 417 # Translate command as a text into a list such as 418 # complex commands can be built 419 set r [split $command "\n"] 420 break 421 } 422 } 423 424 return $r 425} 426 427 428# 429# Get command list to reset an interface to a known state 430# 431# Input: 432# - eq : equipment fqdn 433# - type : equipment type (ex: cisco, juniper...) 434# - model : equipment model (ex: 3750, M20...) 435# - iface : interface name 436# Output: 437# - return value : list of commands to send 438# 439# History : 440# 2010/09/23 : pda/jean : design 441# 442 443proc resetif {eq type model iface} { 444 # 445 # Get command list to reset interface 446 # 447 448 set lcmd {} 449 foreach fmt [fetch-cmd $type $model "ifreset"] { 450 lappend lcmd [format $fmt $iface] 451 } 452 453 set l2 [fetch-cmd $type $model "resetvlan"] 454 if {[llength $l2]>0} then { 455 foreach vlan [get-vlans $eq $iface] { 456 foreach fmt $l2 { 457 lappend lcmd [format $fmt $iface $vlan] 458 } 459 } 460 } 461 462 # 463 # Get command to enable interface 464 # 465 466 foreach fmt [fetch-cmd $type $model "ifenable"] { 467 lappend lcmd [format $fmt $iface] 468 } 469 470 return $lcmd 471} 472 473# 474# Get vlan list for an equipment and an interface 475# 476# Input: 477# - eq : equipment fqdn 478# - iface : interface name 479# Sortie 480# - return value : list of found vlanid 481# 482# History : 483# 2010/09/23 : pda/jean : design 484# 485 486proc get-vlans {eq iface} { 487 global conf 488 489 set lvlans {} 490 491 # XXX remove domain name 492 regsub {\..*} $eq "" eqname 493 set cmd [format $conf(extracteq) $eqname $iface] 494 495 if {[call-topo $cmd msg]} then { 496 foreach line [split $msg "\n"] { 497 if {[lindex $line 0] eq "iface"} then { 498 foreach vlan [lreplace $line 0 6] { 499 lappend lvlans [lindex $vlan 0] 500 } 501 } 502 } 503 } else { 504 puts stderr "extracteq : $msg" 505 } 506 507 return $lvlans 508} 509 510# 511# Check equipment reachability 512# 513# Input: 514# - eq : equipment fqdn 515# Output: 516# - return value : empty string or error message 517# 518# History 519# 2010/10/14 : pda/jean : split in a function 520# 521 522proc test-ping {eq} { 523 global ctxt 524 525 set cmd [format $ctxt(fpingcmd) $eq] 526 if {[catch {exec sh -c $cmd} msg]} then { 527 set r $msg 528 } else { 529 set r "" 530 } 531 532 return $r 533} 534 535# 536# Keep a log of configuration attempt 537# 538# Input: 539# - eq : equipment name 540# - msg : error message 541# Output: 542# - none 543# 544# History : 545# 2010/10/14 : pda/jean : design 546# 547 548proc update-modlog {eq msg} { 549 global ctxt 550 551 set qmsg [::pgsql::quote $msg] 552 set qeq [::pgsql::quote $eq] 553 set sql "UPDATE topo.ifchanges 554 SET modlog = '$qmsg', moddate = now () 555 WHERE eq = '$qeq' AND processed = 0" 556 if {! [toposqlexec $sql]} then { 557 log-error "Cannot update modlog for eq=$eq" 558 } 559 560 return 561} 562 563# 564# Mark modification requests as processed, with log of lines sent to 565# the equipment. 566# 567# Input: 568# - eq : equipment name 569# - lreqdate : list of request dates 570# - msg : log message 571# Output: 572# - aucune 573# 574# History : 575# 2010/10/14 : pda/jean : design 576# 577 578proc mark-processed {eq lreqdate msg} { 579 global ctxt 580 581 set reqdate [join $lreqdate "', '"] 582 583 set qmsg [::pgsql::quote $msg] 584 set qeq [::pgsql::quote $eq] 585 set sql "UPDATE topo.ifchanges 586 SET processed = 1, modlog = '$qmsg', moddate = now () 587 WHERE eq = '$qeq' AND reqdate IN ('$reqdate')" 588 if {! [toposqlexec $sql]} then { 589 log-error "Cannot update 'processed' flag for eq=$eq" 590 } 591} 592 593############################################################################## 594# Reread network graph 595############################################################################## 596 597# 598# Read or re-read network graph to get equipment types 599# 600# Input: 601# - force : force reread 602# - graph : name of file containing network graph 603# - _tabeq : name of array containing, in return, types and models 604# Output: 605# - return value: 1 (ok) or 0 (0) error 606# - tabeq : array, indexed by FQDN of equipement, containing: 607# tabeq(<eq>) {<type> <model>} 608# 609# History 610# 2010/12/23 : pda : design 611# 612 613proc reread-eq-type {force graph _tabeq} { 614 upvar $_tabeq tabeq 615 616 set fmod [detect-filemod $graph] 617 switch [lindex $fmod 0] { 618 {} { 619 set msg "" 620 } 621 err { 622 set msg [lindex $fmod 2] 623 set force 0 624 } 625 del { 626 set msg "$graph disappeared" 627 set force 0 628 } 629 default { 630 set force 1 631 } 632 } 633 634 if {$force} then { 635 set msg [read-eq-type tabeq] 636 if {$msg eq ""} then { 637 sync-filemonitor [list $fmod] 638 } 639 } 640 641 if {$msg eq ""} then { 642 set r 1 643 set msg "Resuming normal operation" 644 } else { 645 set r 0 646 } 647 648 keep-state-mail "graphread" $msg 649 650 return $r 651} 652 653############################################################################## 654# Main program 655############################################################################## 656 657# The -z option is reserved for internal use 658set usage {usage: %1$s [-h][-f][-v <n>] 659 -h : display this text 660 -f : run in foreground 661 -v <n> : verbose level (0 = none, 1 = minimum, 99 = max) 662} 663 664proc usage {argv0} { 665 global usage 666 667 puts stderr [format $usage $argv0] 668} 669 670# 671# Main program 672# 673 674proc main {argv0 argv} { 675 global ctxt 676 677 set ctxt(dbfd1) "" 678 set ctxt(dbfd2) "" 679 set ctxt(send) 0 680 set verbose 0 681 set foreground 0 682 set daemonized 0 683 684 # 685 # Get configuration values from local file 686 # 687 688 set-log [get-local-conf "logger"] 689 690 set ctxt(fpingcmd) [get-local-conf "fpingcmd"] 691 692 set graph [get-local-conf "topograph"] 693 694 # 695 # Get configuration values from database 696 # 697 698 config ::dnsconfig 699 lazy-connect 700 701 set delay [dnsconfig get "toposendddelay"] 702 set delay [expr $delay*1000] 703 704 set ctxt(maxstatus) [dnsconfig get "topomaxstatus"] 705 706 set ctxt(ifchangeexpire) [dnsconfig get "ifchangeexpire"] 707 708 # 709 # Argument analysis 710 # 711 712 while {[llength $argv] > 0} { 713 switch -glob -- [lindex $argv 0] { 714 -h { 715 usage $argv0 716 return 0 717 } 718 -f { 719 set foreground 1 720 set argv [lreplace $argv 0 0] 721 } 722 -z { 723 # This option is not meant to be used by a human 724 # It implies that the program is being rerun in order to be 725 # daemonized 726 set daemonized 1 727 set argv [lreplace $argv 0 0] 728 } 729 -v { 730 set verbose [lindex $argv 1] 731 set argv [lreplace $argv 0 1] 732 733 } 734 -* { 735 usage $argv0 736 return 1 737 } 738 default { 739 break 740 } 741 } 742 } 743 744 if {[llength $argv] != 0} then { 745 usage $argv0 746 return 1 747 } 748 749 if {! $foreground && ! $daemonized} then { 750 set argstr {} 751 if {$verbose > 0} then { 752 lappend argstr -v $verbose 753 } 754 lappend argstr "-z" 755 run-as-daemon $argv0 [join $argstr " "] 756 } 757 758 reset-status 759 set-status "Starting toposendd" 760 761 # 762 # Default values 763 # 764 765 topo-set-verbose $verbose 766 767 if {$verbose > 0} then { 768 set-trace {toposqlselect toposqlexec toposqllock toposqlunlock 769 keep-state-mail 770 read-eq-type send-changes mod-to-conf 771 execute-cmd fetch-cmd resetif get-vlans test-ping 772 update-modlog mark-processed} 773 } 774 775 # 776 # Daemon loop 777 # 778 779 set first 1 780 set forcereread 1 781 782 while {true} { 783 # 784 # Except first time, wait for the delay 785 # 786 787 topo-verbositer "delay : first=$first delay=$delay" 10 788 if {! $first} then { 789 after $delay 790 } 791 set first 0 792 793 # 794 # Check if equipment types must be (re)read 795 # 796 797 if {! [reread-eq-type $forcereread $graph tabeq]} then { 798 continue 799 } 800 set forcereread 0 801 802 # 803 # Get modification requests from Web interface and send them 804 # 805 806 send-changes tabeq 807 } 808} 809 810exit [main $argv0 $argv] 811