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