# # Copyright (C) 2010 Alexandros Stergiakis <alsterg@gmail.com> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # #//# # Procedures for handling MikroConf command specifications. # # This file encapsulates procedures and data structures for the following: # * registering new commands/attributes/confmodes/authmodes # * finding information about registered commands/attributes # * internal representation of the command line hierarchy and confmodes/authmodes. # * running system configuration data structure # * retrieve running/startup configuration # * save running config, load startup config # # @assume Dependency on the structure of the DOM tree. #//# package require tdom 0.8.2 package require struct::set 2.2.1 package require struct::list 1.6.1 package require grammar::fa 0.3 package require grammar::fa::op 0.4 namespace eval sysconf { namespace import ::session::close_session ::session::create_bg_session ::module::api::ferror namespace export loadspecs remove print match authmode confmode execute runningConf startupConf loadConf saveConf inuse # The DOM data structure that stores the command specifications. dom createDocumentNode sysconf ################# # Load/Remove/Print Specifications ################# # Load command specifications from an XML file into the internal data representation. # In case of parse or other error, we catch them in here, and log it. # # @param filepath The source file itself. # @error proc loadspecs {filepath} { Global BASE_DIR log::Debug "Loading command specs from $filepath" variable sysconf variable parser [expat \ -namespace \ -final 1 \ -baseurl [tDOM::baseURL $BASE_DIR/] \ -externalentitycommand tDOM::extRefHandler \ -paramentityparsing always \ -ignorewhitecdata 1] #package require tnc 0.3.0 #tnc $parser enable ;# for DTD validation tdom $parser enable if {[catch { $parser parsefile $filepath } errMsg errStack]} { tdom $parser remove $parser free log::Critical -error -stack $errStack "Failed to parse command specifications $filepath: " $errMsg } set doc [tdom $parser getdoc] $doc documentElement root $sysconf appendChild $root $sysconf deleteXPathCache $doc delete ;# Also deletes any node command } # Check if a module is in use by some session (user or background or remote), or not. # # @return '1' if in use, '0' otherwise. proc inuse {name} { variable sysconf set nodes [$sysconf selectNodes [format {/module[@name='%s']} $name]] # Check first to see if a session is currently on an Authmode or Confmode # owned by this module. If so then we cannot unload it. foreach node $nodes { foreach sid [concat [::session::bg_sessions] [::session::user_sessions]] { Session $sid CONFMODE AUTHMODE if {[$node selectNodes [format {boolean(confmode[@name='%s'])} $CONFMODE]] || [$node selectNodes [format {boolean(authmode[@name='%s'])} $AUTHMODE]]} { return 1 } } } return 0 } # Remove all command specifications associated with a module name. # # @param name The module name. proc remove {name} { variable sysconf log::Debug "Unloading command specs for module: $name" set nodes [$sysconf selectNodes [format {/module[@name='%s']} $name]] foreach node $nodes { $node delete } } # Return the running configuration as XML, or a subparts of it, depetermined by a XPath query. # # @param arguments The arguments accepted by the 'asXML' sub-command of domDoc/domNode. # @param query XPath query to determine the nodes whose subtrees will be concatanated and printed. # @return An XML string. proc print {{arguments {}} {query {}}} { variable sysconf if {$query eq {}} { return [$sysconf asXML {*}$arguments] } else { set ret {} set nodes [$sysconf selectNodes $query] foreach node $nodes { append ret [$node asXML {*}$arguments] "\n" } return $ret } } ################# # Command/Argument matching ################# # It looks for MikroConf commands and arguments that can be placed # in the word that the cursor is pointing at, and have as prefix the existing # word part. # # Matching of commands is based solely on their names. # # An argument can be of type: # 1. exact : One possible value is permitted, and this is the name of the argument # as found in the name=".." parameter of the argument tag. This argument type # can be abbreviated. # 2. list : A fixed finite list of alternatives that is defined in the XML specifications # for the command. This argument type can be abbreviated. # 3. dlist : A dynamic finite list of alternatives that is automatically generated on # runtime. This argument type can be abbreviated. # 4. any : Any value fits in this category. The validity is determined by the command # handler on execution. This argument type cannot be abbreviated. It can coexist # with the previous types, in which case if there is no exact match with any of the # exact/list/dlist alternatives, it is caught by this one, however if there is an exact # match, it takes precedence. It is not considered by TAB expansion, which only # considers types 1-3. In-line help (?) on the other hand always lists it, along with # any matching types of 1-3. # 5. fixed : The value of this element must hold a serialized Tcl array mapping # names to help messages. Command expansion (TAB) will consider the # names as valid alternatives, whereas in-line help (with ?) will display the # names as alternatives along with the corresponding help messages. The # "fixed" type is useful for brevity purposes, and performance. Instead of # having a complex automaton with multiple "exact" element types, you # only have one element and only one state in the automaton. # # All words except the last one are expected to be unambiguous, meaning that # (after aplying proper precedence rules) match to exactly one possible entity # (argument or command). # # To resolve collisions between literals coming from "list" "dlist" "fixed" or "exact" types # with the "any" type that accepts any string, we give precedence to the former types, if and # only if there is an exact match. For example "running-config" will match the "exact" type # with name "running-config", but "running" will match the "any" type if it is supported in the # argument position being processed. # # For the last argument the procedure returns information based on a list of provided flags. # # Supported flags: # -no : Indicates whether the command is a negation (starts with no) or not. # -type : For commands it returns the string "command" and for arguments the string "argument". # -pos : The position in the command list when the last word that is interpreted as command resides. # After this point, all remaining words are arguments. # -nodes : For commands it returns the matching nodes, INCLUDING structural paths. # For arguments, it returns the single command node the arguments belongs to. # -count : For commands it returns the number of matching commands, EXCLUDING structural paths. # For arguments, it returns the number of matching arguments, of all types; it does not consider # type precedence (e.g. "exact" has higher precedence then "any" type). # -names : Returns matching names (command & argument names). For commands structural nodes # are not considered. # -values : Returns matching values (command names & argument values). # This is used by Tab completion. # -types : The types of the arguments that are valid for the specified argument position. # -help : Return a list of tuples for each matching argument/command of the form: {Name Description}. # For commands structural nodes are not considered. This is used by question mark help. # -final : 1 indicates that the command can be executed as it is. # 0 that it is not specific enough or complete yet. # -vector : Return a list of tuples of the form: <argname value>, one for each argument in cmdlist. # <argname> is the unique name of the argument and <value> is the value provided in the cmdlist # by the user. # # Note: "no" is counted in -count, -help, -values, -names, but not in -vector. # # The command supports returning information for multiple flags with a single call. This is # for performance reasons. It is more efficient to run the algorithm once and retrieve as much # information as possible, than running it multiple times for different types of info. # When multiple flags are specified then the result is a list of lists. One sub-list for each flag in the # order they are provided (from left to right). # # Note: Any backslashes in the input command string are substituted (removed) first before processing it. # # @param sid Session Id. # @param options A combination of: "-count", "-names", "-values", "-help", "-final" # @param cmdline The command line string to operate on. # @param wordIndex The word index the cursor is pointing at in the cmdline list. # @return Returns the matched names/values for each provided flag in order. # @error proc match {sid options cmdline wordIndex} { variable sysconf Session $sid CONFMODE set AUTHPRIV [authmode $sid privilege] # Substitute backslashes. This removes the end-of-line escapes. set cmdline [subst -nocommands -novariables $cmdline] # Just to make sure that backslash substitutions didn't result in a non-command. # Note: "puts {a}n" is a complete command (info complete), but NOT a list (string is list). if {! [info complete $cmdline] || ! [string is list $cmdline]} { ;# the empty string is a list error "command string is not a valid command" } # Can happen if TAB/? is pressed on a "\" #if {[lindex $cmdline $wordIndex] eq "\\"} { return } # The configuration modes within which we are searching for a match # 'pool' always holds the matching nodes up to the current point of processing. set pool [$sysconf selectNodes [format {/module/tree[@conf='%s'][@priv<=%s]} $CONFMODE $AUTHPRIV]] if {[lempty $pool]} { ;# No commands for current Authmode/Confmode combination. error "no commands within scope" } # Skip leading "\" #if {$i eq "\\"} { incr i } # Indicates that the command is a negation. set NO 0 # Match against command names first for {set i 0} {$i <= $wordIndex} {incr i} { # Note: wordIndex can point after the end of the cmdline (wordIndex == [llength $cmdline] + 1), # see pos2index for details. However, there is no problem with the following algorithm because # [lindex {a b} 3] == {}, and, starts-with(anything, {}), matches. set prefix [lindex $cmdline $i] # Check if it is a negative command. It must start with a "no" word. # Note: "no" cannot be abbreviated. # Note: if $wordIndex == $i it means we refer to "no" and looking for alternatives there. if {$i == 0 && $prefix eq "no" && $wordIndex > $i} { set NO 1 continue } # Skip "\" #if {$prefix eq "\\"} { continue } # The node pool for the next iteration. set all [list] foreach n $pool { if {$NO} { # Select all commands that have the right prefix & permission & (for negations) support "no" # With priv: lappend all {*}[$n selectNodes [format {command[starts-with(@name, '%s')][@priv<=%s]/no/..} $prefix $AUTHPRIV]] lappend all {*}[$n selectNodes [format {command[starts-with(@name, '%s')]/no/..} $prefix]] # Select all paths that have the right prefix and lead to at least one command that has the right permission and (for negations) supports "no" set all2 [$n selectNodes [format {path[starts-with(@name, '%s')]} $prefix]] foreach n2 $all2 { if {[$n2 selectNodes {boolean(.//command/no)}]} { lappend all $n2 } } # With priv attribute on commands: #set all2 [$n selectNodes [format {path[starts-with(@name, '%s')]} $prefix]] #foreach n2 $all2 { # if {[$n2 selectNodes [format {boolean(.//command[@priv<=%s]/no)} $AUTHPRIV]]} { # lappend all $n2 # } #} } else { # Select all commands that have the right prefix & permission # With priv: lappend all {*}[$n selectNodes [format {command[starts-with(@name, '%s')][@priv<=%s]} $prefix $AUTHPRIV]] lappend all {*}[$n selectNodes [format {command[starts-with(@name, '%s')]} $prefix]] # Select all paths that have the right prefix and lead to at least one command that has the right permission lappend all {*}[$n selectNodes [format {path[starts-with(@name, '%s')]} $prefix]] # With priv attribute on commands: #set nodes [$n selectNodes [format {path[starts-with(@name, '%s')]} $prefix]] #foreach n2 $nodes { # if {[$n2 selectNodes [format {boolean(.//command[@priv<=%s])} $AUTHPRIV]]} { # lappend all $n2 # } #} } } catch { unset nodes } # Filter out those paths that have no description, meaning that they don't _define_ a path, but rather have a structural role. # Note: We require that when a structural path matches, the original also matches. This in practice is guaranteed with # module dependencies: By having the modules that use a path to depend on the module that defines it. # @assume Inter-module dependencies expressed in the form of 'module require' should guarantee that when a # structural path exists, its original also exists (the owner). set unique [list] foreach n $all { if {[$n selectNodes {boolean(desc)}] || [$n selectNodes {boolean(no/desc)}]} { lappend unique $n } } # Count unique matches set count [llength $unique] # Check if we reached the end of the string without encountering an argument. if {$i == $wordIndex} { set Result [list] foreach option $options { set result [list] switch -exact -- $option { "-no" { set result $NO } "-type" { set result "command" } "-pos" { set result {} ;# We haven't encountered the last argument yet. } "-nodes" { # Note: We match structural paths. set result $all } "-count" { # Note: We don't match structural paths. set result [llength $unique] if {$i == 0 && [string match "${prefix}*" "no"]} { foreach n $pool { # With priv: #if {[$n selectNodes [format {boolean(.//command[@priv<=%s]/no)} $AUTHPRIV]]} if {[$n selectNodes {boolean(.//command/no)}]} { incr result break } } } } "-names" - "-values" { # Note: We don't match structural paths. foreach n $unique { lappend result [$n selectNodes {string(@name)}] } # Append "no" if there is an accessible command that supports negation. if {$i == 0 && [string match "${prefix}*" "no"]} { foreach n $pool { # With priv: #if {[$n selectNodes [format {boolean(.//command[@priv<=%s]/no)} $AUTHPRIV]]} if {[$n selectNodes {boolean(.//command/no)}]} { lappend result "no" break } } } set result [lsort $result] } "-help" { # Note: We don't match structural paths. foreach n $unique { if {$NO && [$n selectNodes {boolean(no/desc)}]} { ;# If it doesn't have no/desc, use desc instead. lappend result [list [$n getAttribute name] [$n selectNodes {string(no/desc/text())}]] } else { lappend result [list [$n getAttribute name] [$n selectNodes {string(desc/text())}]] } } # Append "no" if there is an accessible command that supports negation. if {$i == 0 && [string match "${prefix}*" "no"]} { foreach n $pool { # With priv: #if {[$n selectNodes [format {boolean(.//command[@priv<=%s]/no)} $AUTHPRIV]]} if {[$n selectNodes {boolean(.//command/no)}]} { lappend result [list no {Negate a command; cancel its effect; or restore default behavior}] break } } } # Sort. set result [lsort -index 0 $result] } "-vector" { # We haven't reached arguments, so no vector. } "-types" { # We haven't reached arguments, so no types. } "-final" { # Special Case: # We pressed ? on an empty word, and there is only one option for this word available # which is also final. Therefore without taking $prefix into consideration, ENTER will # be displayed although the user hasn't entered anything in this word. set result [expr {! [lempty $prefix] && [isCmdFinal $NO $unique]}] } default { log::Error -error "Unsupported sub-command" } } ;# switch if {[llength $options] > 1} { lappend Result $result } else { set Result $result } } ;# foreach return $Result } if {$count == 0} { ferror "unknown command at the indicated position" $i } elseif {$count > 1} { # Retrive node names set names [list] foreach n $unique { lappend names [$n getAttribute name] } ferror "ambiguous command at the indicated position; ambiguitiy between:\n $names" $i } else { ;# == 1 if {[$unique nodeName] eq "command"} { # Next word will be an argument set pool $unique set lastcmd_pos $i incr i ;# since we break, 'i' is not increased by 'for' to point to next word, as it would otherwise. break } else { set pool $all } } } ;# match next word, which is a command or path catch { unset all unique count names } # To reach here it means that we point to an argument. # The next word in the cmdline is an argument (pointed by $i now), followed by (possibly) more arguments. # These variables have value from before: i, wordIndex, pool, NO, options, cmdline, lastcmd_pos # Case: Command does not support arguments, but an argument is supplied. if {(! $NO && ! [$pool selectNodes {boolean(syntax)}]) || \ ($NO && ! [$pool selectNodes {boolean(no/syntax)}])} { if {$i == $wordIndex} { ;# Last word. # Special Case: # A ? is pressed on an empty word after a final command. # Since we reach here, it means that the previous word is a command. # Thus the ? is refering to its first argument, but the command doesn't # support arguments. Enter is possible at this point, because no # argument is provided yet, and no argument is required. if {"-final" in $options && [lempty [lindex $cmdline $i]] && [isCmdFinal $NO $pool]} { set res {} foreach opt $options { switch -exact -- $opt { "-final" { lappend res 1 } "-pos" { lappend res [expr {[llength $cmdline] - 1}] } "-count" { lappend res 0 } "-no" { lappend res $NO } "-type" { lappend res "command" } default { lappend res {} } } } return $res } return ;# signifies unnecessary argument. } else { ;# There are more words. ferror "unnecessary argument at indicated position" $i } } compute_fa fa $NO $pool set startstates [fa startstates] set finalstates [fa finalstates] set state $startstates ;# A deterministic automaton has always only one start state. # -1 Indicates that previous word was a command. # 1 Indicates that previous word was an argument and it was final. # 0 Indicates that previous word was an argument and it was NOT final. set prevFinal -1 # Now match arguments for {} {$i <= $wordIndex} {incr i} { set prefix [lindex $cmdline $i] # Skip "\" #if {$prefix eq "\\"} { continue } # Validity check: Only one argument of type "any" is allowed. set alternatives [fa symbols@ $state] set any_type_no 0 foreach a $alternatives { if {$NO && [$pool selectNodes [format {boolean(no/argument[@name='%s']/type[@name='any'])} $a]]} { ;# give priority to arguments under <no> incr any_type_no [$pool selectNodes [format {boolean(no/argument[@name='%s']/type[@name='any'])} $a]] } else { incr any_type_no [$pool selectNodes [format {boolean(argument[@name='%s']/type[@name='any'])} $a]] } } if {$any_type_no > 1} { fa destroy ferror "inherently ambiguous argument" $i } unset any_type_no set argList [list] ;# {type name values} , .... array set fixedList {} ;# name => value of type "fixed", which is actually an array of value => help foreach name $alternatives { if {$NO && [$pool selectNodes [format {boolean(no/argument[@name='%s'])} $name]]} { ;# give priority to arguments under <no> set type [$pool selectNodes [format {string(no/argument[@name='%s']/type/@name)} $name]] set value [$pool selectNodes [format {string(no/argument[@name='%s']/type/text())} $name]] } else { set type [$pool selectNodes [format {string(argument[@name='%s']/type/@name)} $name]] set value [$pool selectNodes [format {string(argument[@name='%s']/type/text())} $name]] } switch -exact -- $type { "exact" { if {$prefix eq $name} { lappend argList [list "match" $name $name] } elseif {[string match ${prefix}* $name]} { lappend argList [list "exact" $name $name] } } "list" { set matches [list] foreach v $value { if {$prefix eq $v} { lappend argList [list "match" $name $v] } elseif {[string match ${prefix}* $v]} { lappend matches $v } } if {! [lempty $matches]} { lappend argList [list "list" $name $matches] } unset matches } "dlist" { set modname [$pool selectNodes {string(ancestor-or-self::module/@name)}] if {[catch { # Evaluate dlist handler in its module's namespace. namespace inscope ::module::$modname {*}$value $sid } result errStack]} { fa destroy log::Alert -error -stack $errStack "Failed to retrieve dynamic list for argument \"$name\" in command \"$cmdline\" :" $result } unset modname set matches [list] foreach v $result { if {$prefix eq $v} { lappend argList [list "match" $name $v] } elseif {[string match ${prefix}* $v]} { lappend matches $v } } lappend argList [list "dlist" $name $matches] unset matches } "fixed" { array set arr $value foreach n [array names arr] { if {$prefix eq $n} { lappend argList [list "match" $name $n] unset arr($n) } elseif {! [string match ${prefix}* $n]} { unset arr($n) } } if {! [lempty [array names arr]]} { lappend argList [list "fixed" $name [array names arr]] set fixedList($name) [array get arr] } unset arr } "any" { lappend argList [list "any" $name {}] } default { fa destroy log::Alert -error "unknown argument type \"$type\"" } } ;# switch } catch { unset type name value alternatives } set count [llength $argList] lassign {} types names values for {set n 0} {$n < $count} {incr n} { lappend types [lindex $argList $n 0] lappend names [lindex $argList $n 1] lappend values {*}[lindex $argList $n 2] ;# {*} because it can be empty, and we don't want empty lists. } # Break if we reached the last word. # The reason we have this here, is because we want to perform the # previous computations also when we process the last word. if {$i == $wordIndex} { break } # We save this information because in order to determine if an argument position # is final, we need it to know if the previous argument position was final. # For example when we press ? on an empty last word on the command line, and the # previous argument was final. set prevFinal [isArgFinal $NO $pool $argList $prefix $count $types $values $names $startstates $finalstates $state $prevFinal] if {$count == 0} { fa destroy ferror "erroneous argument at the indicated position" $i } elseif {$count > 1} { set matches [llength [lsearch -exact -all $types "match"]] # Many matches should not happen. if {$matches > 1} { ferror "inherently ambiguous argument" $i # Exact match takes precedence. } elseif {$matches == 1} { set name [lindex $names [lsearch -exact $types "match"]] lappend vector $name $prefix set state [fa next $state $name] unset name # Otherwise "any" catches all. } elseif {"any" in $types} { # We accept it since it matches on the "any" type. set name [lindex $names [lsearch -exact $types "any"]] ;# We know that is in the same list possition. lappend vector $name $prefix ;# store the actual value entered on the command line. set state [fa next $state $name] unset name # Without any and exact match, it's an error. } else { fa destroy ferror "ambiguous argument at the indicated position; ambiguitiy between:\n $names" $i } } else { ;# == 1 if {$types eq "any"} { lappend vector $names $prefix } else { lappend vector $names $values } set state [fa next $state $names] } } unset i fa destroy ;# For good. # Now process the last argument. Since $wordIndex can point after $cmdline, this last argument # can be non-existing. # These variables have value from before: # pool, wordIndex, state, names, types, values, count, prevFinal, startstates, finalstates, vector set Result [list] foreach option $options { set result [list] switch -exact -- $option { "-no" { set result $NO } "-type" { set result "argument" } "-types" { set result $types } "-pos" { set result $lastcmd_pos } "-nodes" { set result $pool } "-count" { # We count all types. This cannot be used as to determine if a command is complete. # Use -final for this. set result $count } "-names" { set result $names } "-values" { # The "any" type cannot be returned in any meaningful form. set result [lsort $values] } "-help" { foreach n $names { if {[info exists fixedList($n)]} { foreach {n2 desc} $fixedList($n) { lappend result [list $n2 $desc] } } else { if {$NO && [$pool selectNodes [format {boolean(no/argument[@name='%s']/desc)} $n]]} { set desc [$pool selectNodes [format {string(no/argument[@name='%s']/desc/text())} $n]] } else { if {$NO && [$pool selectNodes [format {boolean(argument[@name='%s']/nodesc)} $n]]} { ;# If it doesn't have nodesc, use desc instead. set desc [$pool selectNodes [format {string(argument[@name='%s']/nodesc/text())} $n]] } else { set desc [$pool selectNodes [format {string(argument[@name='%s']/desc/text())} $n]] } } lappend result [list $n $desc] } } set result [lsort -index 0 $result] } "-vector" { if {[llength $values] == 1} { ;# exact match # Find the name whose one of its values is $values. for {set t 0} {$t < $count} {incr t} { if {$values in [lindex $argList $t 2]} { lappend vector [lindex $argList $t 1] $values break } } } elseif {"any" in $types} { set n [lindex [lsearch -exact -inline -index 0 $argList "any"] 1] ;# Find the name that has type "any". lappend vector $n $prefix ;# store the actual value entered on the command line. } else { # To mark that it is not determined yet, we add an empty list along with the provided value. lappend vector {} $prefix } set result $vector } "-final" { set result [isArgFinal $NO $pool $argList $prefix $count $types $values $names $startstates $finalstates $state $prevFinal] } default { log::Error -error "Unsupported sub-command" } } ;# switch if {[llength $options] > 1} { lappend Result $result } else { set Result $result } } ;# foreach return $Result } # When the cursor is placed on a path or command, find whether the command line # up and including the word where the cursor is placed, can be executed as it is. # # @param NO Boolean. Whether we have a negation command or not. # @param node The command node in the DOM tree. # @return '1' if command is final, '0' otherwise. proc isCmdFinal {NO node} { # In summary: # llength $node == 1 && node is command && ( without syntax || fa start == fa final) if {[llength $node] == 1 && [$node nodeName] eq "command"} { if {$NO} { set no {no/} } else { set no {} } if {! [$node selectNodes [format {boolean(%ssyntax)} $no]]} { return 1 } # else compute_fa fa1 $NO $node if {[fa1 startstates] in [fa1 finalstates]} { fa1 destroy return 1 } fa1 destroy } return 0 } # When the cursor is placed on an argument, find whether the command line # up and including the word where the cursor is placed, can be executed as it is. # # @param NO Boolean. Whether we have a negation command or not. # @param node The node of the command the argument belongs to. # @param argList List of arguments that match with the prefix: {type name values} .... # @param prefix The prefix of the word the cursor is placed. # @param count The number of matching arguments (or valid arguments). # @param types The types of all valid arguments concatenated. # @param values The matching values of all valid arguments concatenated. # @param names The names of all valid arguments concatenated. # @param startstate The name of the start state of the automaton. # @param finalstates The names of final states of the automaton. # @param curstate The name of the current state. # @param prevFinal Whether the previous word was a command (-1), a final state (1) or else (0). # @return '1' if argument is final, '0' otherwise. proc isArgFinal {NO node argList prefix count types values names startstate finalstates curstate prevFinal} { # Algorithm: # An argument is final in the following cases: # 1. prefix eq "" && previous word is a command && startstate in finalstates # 2. prefix eq "" && previous word is an argument && previous argument is final # 3. prefix ne "" && $values == 1 && (state after name($values)) in finalstates # 4. prefix ne "" && "any" in $types && $values != 1 && (state after name(any)) in finalstates # Anything else results in a non-final argument. # Case 1 & 2. if {[lempty $prefix]} { # Case 1. if {$prevFinal == -1 && $startstate in $finalstates} { return 1 } # Case 2. if {$prevFinal == 1} { return 1 } # Case 3 & 4. } else { # Case 3. if {[llength $values] == 1} { # Find the name whose one of its values is $values. for {set t 0} {$t < $count} {incr t} { if {$values in [lindex $argList $t 2]} { set name [lindex $argList $t 1] if {[nextState $NO $node $curstate $name] in $finalstates} { return 1 } break } } } # Case 4. if {"any" in $types && [llength $values] != 1} { # Find the name that has type "any". set name [lindex [lsearch -exact -inline -index 0 $argList "any"] 1] if {[nextState $NO $node $curstate $name] in $finalstates} { return 1 } } } return 0 } # Returns the state of the automaton after consuming $symbol from $curstate. # # @param NO Boolean. Whether we have a negation command or not. # @param node The node of the command the argument belongs to. # @param curstate The name of the current state. # @param symbol The symbol to consume. # @return The state of the automaton after consuming $symbol from $curstate. proc nextState {NO node curstate symbol} { compute_fa fa2 $NO $node set nextstate [fa2 next $curstate $symbol] ;# Consume $symbol. fa2 destroy return $nextstate } ################################ # Command Execution ################################ # This command attempts to find if a given command is a mikroconf command and if so, it executes it. # # @param command The command name and arguments to execute. # @return The result of the execution or a formatted error message. # @error proc execute {sid cmdline} { Session $sid CONFMODE set AUTHPRIV [authmode $sid privilege] set pos [expr {[llength $cmdline] - 1}] # The following can generate an error, which we let propagate. lassign [match $sid {-no -final -count -vector -nodes -names -type -pos -types} $cmdline $pos] no final count vector node names type position types if {$count eq ""} { ;# 'match' returned nothing, which means: ferror "unnecessary argument at indicated position" $pos } elseif {$count == 0} { ferror "unknown $type at the indicated position" $pos } elseif {$count > 1} { if {$type eq "command"} { ferror "ambiguous command at the indicated position; ambiguitiy between:\n $names" $pos } else { ;# argument if {[llength [lsearch -all $types "match"]] > 1 || [llength [lsearch -all $types "any"]] > 1} { ferror "inherently ambiguous argument" $pos } if {"match" ni $types && "any" ni $types} { ferror "ambiguous argument at the indicated position; ambiguitiy between:\n $names" $pos } } } if {! $final} { if {$type eq "command"} { error "incompete command" } else { ;# argument set p 0 foreach {t v} $vector { ;# type value if {[lempty $t]} { ;# Marks no match against argument at position $p ferror "illegal argument at the indicated position" [expr {$pos + $p}] } incr p } error "insufficient number of arguments" } } set cmd [$node selectNodes {string(exec/text())}] set modname [$node selectNodes {string(ancestor-or-self::module/@name)}] # Create a memory fifo to capture handler output. # Contrary to "return", this method allows for the output up to an error to be recovered, and printed. # If we were using only the return value from the handler, then output that has been appened to the result # before an error, would not be accessible after the error. set out [fifo] fconfigure $out -buffering full -blocking 0 -translation binary # Handler procs should take care of classifying and logging errors using the helper procs in helper.tcl. if {[catch { namespace inscope ::module::$modname {*}$cmd $cmdline $position $sid $out $no $vector } result options]} { # Print the output up to the error. flush $out set result "[read $out]${result}" } else { # Append handler's result to handler's output. if {! [lempty $result]} { puts $out $result } flush $out set result [read $out] } close $out return -options $options $result } ################# # AUTHMODE ################# # Operations on Authmodes. # Supported: init, set, get, parent, list, exists, getstring, setstring, privilege, exit. # # @param sid Session Id. # @param op Operation. # @param mode The authentication mode name or string, depending on the operation. # @error proc authmode {sid op {mode {}}} { variable sysconf Session -rw $sid AUTHMODE AUTHROOT AUTHSTRING POLICY switch -exact -- $op { "init" { sysconf authmode $sid set $AUTHROOT } "set" { ;# 'mode' is the authmode to switch to. if {! [authmode $sid exists $mode]} { log::Error -error "Authmode \"$mode\" does not exist, while switching authmode" } set AUTHMODE $mode set AUTHSTRING [$sysconf selectNodes [format {string(/module/authmode[@name='%s']/@string)} $AUTHMODE]] # Enforce policy that corresponds to newly set AUTHMODE. if {! [lempty $POLICY]} { Thread::bgsend $sid [list policy $POLICY] } else { Thread::bgsend $sid [list policy $mode] } return } "get" { ;# Get current authmode return $AUTHMODE } "parent" { ;# 'mode' _if_ set is the authmode to look for the parent of. if {[lempty $mode]} { set mode $AUTHMODE } if {$mode eq $AUTHROOT} { log::Warning -error "Root authmode has no parent" } set node [$sysconf selectNodes [format {/module/authmode[@name='%s']} $mode]] if {[llength $node] != 1} { log::Critical -error "Multiple AUTHMODEs with same name were found:" $node } return [$node getAttribute parent] } "list" { ;# Get all registered authmodes set ret [list] set nodes [$sysconf selectNodes {/module/authmode}] foreach node $nodes { lappend ret [$node getAttribute name] } return $ret } "exists" { ;# 'mode' is the name of the authmode that we test if it exists. return [$sysconf selectNodes [format {boolean(/module/authmode[@name='%s'])} $mode]] } "getstring" { ;# Get display string associated with an the current authmode return $AUTHSTRING } "setstring" { ;# 'mode' is the new display string of the current authmode to set to. # It has effect only on the local session set AUTHSTRING $mode } "privilege" { ;# Get privilege level of current authmode return [$sysconf selectNodes [format {string(/module/authmode[@name='%s']/@privilege)} $AUTHMODE]] } "exit" { ;# Exit current authmode and move to the parent # Ignore if already on root if {$AUTHMODE eq $AUTHROOT} { return } authmode $sid set [authmode $sid parent] } default { error "Unsupported sub-command" } } } ################# # CONFMODE ################# # Operations on Confmodes # Supported: init, set, get, parent, list, exists, getstring, setstring, exit, store # # @param sid Session Id. # @param op Operation. # @param mode The configuration mode name or string, depending on the operation. # @error proc confmode {sid op {mode {}}} { variable sysconf Session -rw $sid CONFMODE CONFROOT CONFSTRING USER CONFSTORE switch -exact -- $op { "init" { sysconf confmode $sid set $CONFROOT } "set" { ;# 'mode' is the confmode to switch to. if {! [confmode $sid exists $mode]} { log::Error -error "Confmode \"$mode\" does not exist, while switching confmode" } set CONFMODE $mode set CONFSTRING [$sysconf selectNodes [format {string(/module/confmode[@name='%s']/@string)} $CONFMODE]] return } "get" { ;# Get current confmode return $CONFMODE } "parent" { ;# 'mode' _if_ set is the confmode to look for the parent of. if {[lempty $mode]} { set mode $CONFMODE } if {$mode eq $CONFROOT} { log::Warning -error "Already in root confmode" } set node [$sysconf selectNodes [format {/module/confmode[@name='%s']} $mode]] if {[llength $node] != 1} { log::Critical -error "Multiple CONFMODEs with same name were found:" $node } return [$node getAttribute parent] } "list" { ;# Get all registered confmodes set ret {} set nodes [$sysconf selectNodes {/module/confmode}] foreach node $nodes { lappend ret [$node getAttribute name] } return $ret } "exists" { ;# 'mode' is the name of the confmode that we test if it exists. return [$sysconf selectNodes [format {boolean(/module/confmode[@name='%s'])} $mode]] } "getstring" { ;# Get display string associated with an the current confmode return $CONFSTRING } "setstring" { ;# 'mode' is the new display string of the current confmode to set to. # It has effect only on the local session set CONFSTRING $mode } "store" { if {[lempty $mode]} { return $CONFSTORE } else { set CONFSTORE $mode } } "exit" { ;# Exit current confmode and move to the parent # Terminate session if already on root. # Only user sessions can terminate this way. if {$CONFMODE eq $CONFROOT && $USER} { ::session::close_session $sid "User request" } else { confmode $sid set [confmode $sid parent] } } default { error "Unsupported sub-command" } } } ################################ # MainConf ################################ # Generate the Running Configuration. # # @param parent The mainconf section to generate the running configuration for. # @return The Running Configuration for $parent. # @error proc runningConf {{parent root}} { variable sysconf set result "" ;# The running configuration string. set section_n [$sysconf selectNodes [format {/module/mainconf[@parent='%s']} $parent]] set print_n [$sysconf selectNodes [format {.//command/print[@section='%s']} $parent]] set children [list] foreach n $section_n { # Order Type Parent-node Module-name Section-name lappend children [list \ [$n selectNodes {string(@order)}] \ {section} \ $parent \ [$n selectNodes {string(ancestor-or-self::module/@name)}] \ [$n selectNodes {string(@name)}]] } foreach n $print_n { # Order Type Parent-node Module-name Handler-code lappend children [list \ [$n selectNodes {string(@order)}] \ {print} \ $parent \ [$n selectNodes {string(ancestor-or-self::module/@name)}] \ [$n selectNodes {string(text())}]] } # Enforce order. set children [lsort -increasing -integer -index 0 $children] # Process a child at a time. foreach child $children { lassign $child order type parent modname value set res {} switch -exact -- $type { "section" { set res [runningConf $value] } "print" { if {[catch { # If 'base' is the module name, then evaluate callback on ::module::base namespace. namespace inscope ::module::$modname {*}$value } res errStack]} { log::Emergency -error -stack $errStack "Failed to retrieve running configuration: " "section:$parent" "module:$modname" "code:$value" $res } } } if {! [lempty $res]} { append result $res } } return $result } # Retrieve Startup Configuration # # @return Startup Configuration # @error proc startupConf {} { Global STARTUP_CONFIG_FILE if {! [file exists $STARTUP_CONFIG_FILE]} { error "Startup configuration is absent" } if {[catch { set text [::fileutil::cat $STARTUP_CONFIG_FILE] } errMsg errStack]} { log::Emergency -error -stack $errStack "Failure while reading Start-up Configuration File: " $errMsg } return $text } # Apply Startup Config, resulting in Running Config == Startup Config. # Log but ignore any errors. # # @assume Existance of "priv" authmode and "global" conf mode. # # @param text Configuration text to apply. If not provided then the startup-config is applied. # @error proc loadConf {{text {}}} { if {[lempty $text]} { Global STARTUP_CONFIG_FILE if {[catch { set text [startupConf] } errMsg errStack]} { log::Emergency -error -stack $errStack "Failed to load Start-up configuration: " $errMsg } } # Create a new session to evaluate running-config. By doing this we make sure that # 1. The interpreter/session is not altered in any way that would make execution of # running-config to fail. # 2. We request that the session enters directly to "configure terminal" mode, as # all running-conf commands belong to this mode, but without explicitly switching # to it. We also enter directly to privileged mode. set sid [create_bg_session CONFIG "priv" "global"] Session $sid SLAVE if {[catch { Thread::bgsend $sid [list $SLAVE eval $text] } errMsg errStack]} { log::Critical -stack $errStack "Startup configuration error: " $errMsg # Report in the form of an event that a startup configuration directive failed to be applied. # Useful to record such failures and maybe respond to them. # # @param The failure message. # @param The error stack of the failure. event generate STARTUP_CONFIG FAILED [list $errMsg $errStack] } close_session $sid "Evaluation of running-config finished" } # Write Running Config to Startup Config # # @error proc saveConf {} { Global TMP_DIR STARTUP_CONFIG_FILE if {[catch { set text [runningConf] } errMsg errStack]} { log::Emergency -error -stack $errStack "Failed to generate Running Configuration: " $errMsg } if {[file exists STARTUP_CONFIG_FILE]} { file copy -force -- $STARTUP_CONFIG_FILE [file join $TMP_DIR $STARTUP_CONFIG_FILE] } if {[catch { set fd [open $STARTUP_CONFIG_FILE w] puts $fd $text close $fd } errMsg errStack]} { file rename -force -- [file join $TMP_DIR $STARTUP_CONFIG_FILE] $STARTUP_CONFIG_FILE catch { close $fd } log::Emergency -error -stack $errStack "Failure while writing Start-up Configuration File: " $errMsg } catch { file delete [file join $TMP_DIR $STARTUP_CONFIG_FILE]] } # Emitted after MikroConf's Startup Configuration is saved. # Can be used from distributions to copy the updated Startup config to a safe place. # # @param The full path to the startup-config file in the file system. event generate STARTUP_CONFIG SAVE $STARTUP_CONFIG_FILE } # Get a Finate Automaton (::grammar::fa) from embedded <syntax> specifications. # Cache the generated fa by inserting it in the memory-resident DOM tree inside a <grammar> tag, which is a sibling of <syntax>. # # @param name Command name. # @param NO Set if looking for the finate automaton that describes the negative form of the command. # @param node The DOM node of the command. # @return The command name of the finate automaton that corresponds to the found <syntax> specification. # @error proc compute_fa {name NO node} { variable sysconf if {$NO} { set no {no/} } else { set no {} } if {! [$node selectNodes [format {boolean(%ssyntax)} $no]]} { log::Critical -error "Node \"[$node getAttribute name]\" does not specify a syntax" } ::grammar::fa $name if {[$node selectNodes [format {boolean(%sgrammar)} $no]]} { # <grammar> is cached. if {[catch { $name deserialize [$node selectNodes [format {string(%sgrammar/text())} $no]] } errMsg errStack]} { $name destroy log::Critical -error -stack $errStack "Failed to deserialize syntax for node \"[$node getAttribute name]\":" $errMsg } } else { # We need to convert <syntax> to <grammar>. if {[catch { $name fromRegex [$node selectNodes [format {string(%ssyntax/text())} $no]] } errMsg errStack]} { $name destroy log::Critical -error -stack $errStack "Failed to parse command syntax \"[$syntax_e nodeValue]\" for node \"[$node getAttribute name]\" :" $errMsg } $name determinize $sysconf createElement grammar grammar_e $grammar_e appendFromList [list #text [$name serialize]] $node appendChild $grammar_e } return $name } namespace ensemble create } ;# End of Namespace