netconf 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,137 @@
1
+ module Netconf
2
+ module RPC
3
+
4
+ MSG_END = "]]>]]>"
5
+ MSG_END_RE = /\]\]>\]\]>[\r\n]*$/
6
+ MSG_CLOSE_SESSION = '<rpc><close-session/></rpc>'
7
+ MSG_HELLO = <<-EOM
8
+ <hello>
9
+ <capabilities>
10
+ <capability>urn:ietf:params:xml:ns:netconf:base:1.0</capability>
11
+ <capability>urn:ietf:params:xml:ns:netconf:base:1.0#candidate</capability>
12
+ <capability>urn:ietf:params:xml:ns:netconf:base:1.0#confirmed-commit</capability>
13
+ <capability>urn:ietf:params:xml:ns:netconf:base:1.0#validate</capability>
14
+ <capability>urn:ietf:params:xml:ns:netconf:base:1.0#url?protocol=http,ftp,file</capability>
15
+ </capabilities>
16
+ </hello>
17
+ EOM
18
+
19
+ module Standard
20
+
21
+ def lock( target )
22
+ rpc = Nokogiri::XML( "<rpc><lock><target><#{target}/></target></lock></rpc>" ).root
23
+ Netconf::RPC.set_exception( rpc, Netconf::LockError )
24
+ @trans.rpc_exec( rpc )
25
+ end
26
+
27
+ def unlock( target )
28
+ rpc = Nokogiri::XML( "<rpc><unlock><target><#{target}/></target></unlock></rpc>" ).root
29
+ @trans.rpc_exec( rpc )
30
+ end
31
+
32
+ def validate( source )
33
+ rpc = Nokogiri::XML( "<rpc><validate><source><#{source}/></source></validate></rpc>" ).root
34
+ Netconf::RPC.set_exception( rpc, Netconf::ValidateError )
35
+ @trans.rpc_exec( rpc )
36
+ end
37
+
38
+ def commit
39
+ rpc = Nokogiri::XML( "<rpc><commit/></rpc>" ).root
40
+ Netconf::RPC.set_exception( rpc, Netconf::CommitError )
41
+ @trans.rpc_exec( rpc )
42
+ end
43
+
44
+ def delete_config( target )
45
+ rpc = Nokogiri::XML( "<rpc><delete-config><target><#{target}/></target></delete-config></rpc>" ).root
46
+ @trans.rpc_exec( rpc )
47
+ end
48
+
49
+ def get_config( *args ) # :yeield: filter_builder
50
+
51
+ source = 'running' # default source is 'running'
52
+ filter = nil # no filter by default
53
+
54
+ while arg = args.shift
55
+ case arg.class.to_s
56
+ when /^Nokogiri/
57
+ filter = case arg
58
+ when Nokogiri::XML::Builder then arg.doc.root
59
+ when Nokogiri::XML::Document then arg.root
60
+ else arg
61
+ end
62
+ when 'Hash' then attrs = arg
63
+ when 'String' then source = arg
64
+ end
65
+ end
66
+
67
+ rpc = Nokogiri::XML("<rpc><get-config><source><#{source}/></source></get-config></rpc>").root
68
+
69
+ if block_given?
70
+ Nokogiri::XML::Builder.with( rpc.at( 'get-config' )){ |xml|
71
+ xml.filter( :type => 'subtree' ) {
72
+ yield( xml )
73
+ }
74
+ }
75
+ end
76
+
77
+ if filter
78
+ f_node = Nokogiri::XML::Node.new( 'filter', rpc )
79
+ f_node['type'] = 'subtree'
80
+ f_node << filter.dup # copy filter, don't mess with the original since it may be re-used
81
+ rpc.at('get-config') << f_node
82
+ end
83
+
84
+ @trans.rpc_exec( rpc )
85
+ end
86
+
87
+ def edit_config( *args ) # :yeield: config_builder
88
+
89
+ toplevel = 'config' # default toplevel config element
90
+ target = 'candidate' # default source is 'candidate' @@@/JLS hack; need to fix this
91
+ config = nil
92
+ options = {}
93
+
94
+ while arg = args.shift
95
+ case arg.class.to_s
96
+ when /^Nokogiri/
97
+ config = case arg
98
+ when Nokogiri::XML::Builder then arg.doc.root
99
+ when Nokogiri::XML::Document then arg.root
100
+ else arg
101
+ end
102
+ when 'Hash' then options = arg
103
+ when 'String' then target = arg
104
+ end
105
+ end
106
+
107
+ toplevel = options[:toplevel] if options[:toplevel]
108
+
109
+ rpc_str = <<-EO_RPC
110
+ <rpc>
111
+ <edit-config>
112
+ <target><#{target}/></target>
113
+ <#{toplevel}/>
114
+ </edit-config>
115
+ </rpc>
116
+ EO_RPC
117
+
118
+ rpc = Nokogiri::XML( rpc_str ).root
119
+
120
+ if block_given?
121
+ Nokogiri::XML::Builder.with(rpc.at( toplevel )){ |xml|
122
+ yield( xml )
123
+ }
124
+ elsif config
125
+ rpc.at( toplevel ) << config.dup
126
+ else
127
+ raise ArgumentError, "You must specify edit-config data!"
128
+ end
129
+
130
+ Netconf::RPC.set_exception( rpc, Netconf::EditError )
131
+ @trans.rpc_exec( rpc )
132
+ end
133
+
134
+ end
135
+
136
+ end # module: RPC
137
+ end # module: Netconf
@@ -0,0 +1,132 @@
1
+ require 'serialport'
2
+
3
+ module Netconf
4
+
5
+ class Serial < Netconf::Transport
6
+
7
+ DEFAULT_BAUD = 9600
8
+ DEFAULT_DATABITS = 8
9
+ DEFAULT_STOPBITS = 1
10
+ DEFAULT_PARITY = SerialPort::NONE
11
+ DEFAULT_RDBLKSZ = (1024*1024)
12
+
13
+ attr_reader :args
14
+
15
+ def initialize( args_h, &block )
16
+ os_type = args_h[:os_type] || Netconf::DEFAULT_OS_TYPE
17
+
18
+ raise Netconf::InitError, "Missing 'port' param" unless args_h[:port]
19
+ raise Netconf::InitError, "Missing 'username' param" unless args_h[:username]
20
+
21
+ @args = args_h.clone
22
+ @args[:prompt] ||= /([%>])\s+$/
23
+
24
+ # extend this instance with the capabilities of the specific console
25
+ # type; it needs to define #trans_start_netconf session
26
+ # this must be provided! if the caller does not, this will
27
+ # throw a NameError exception.
28
+
29
+ extend Netconf::TransSerial::const_get( os_type )
30
+
31
+ @trans_timeout = @args[:timeout] || Netconf::DEFAULT_TIMEOUT
32
+ @trans_waitio = @args[:waitio] || Netconf::DEFAULT_WAITIO
33
+
34
+ super( &block )
35
+ end
36
+
37
+ def login
38
+
39
+ begin
40
+ puts
41
+ waitfor(/ogin:/)
42
+ rescue Timeout::Error
43
+ puts
44
+ waitfor(/ogin:/)
45
+ end
46
+
47
+ puts @args[:username]
48
+
49
+ waitfor(/assword:/)
50
+ puts @args[:password]
51
+
52
+ waitfor( @args[:prompt] )
53
+ end
54
+
55
+ def trans_open # :yield: self
56
+
57
+ baud = @args[:speed] || DEFAULT_BAUD
58
+ data_bits = @args[:bits] || DEFAULT_DATABITS
59
+ stop_bits = @args[:stop] || DEFAULT_STOPBITS
60
+ parity = @args[:parity] || DEFAULT_PARITY
61
+
62
+ @trans = SerialPort.new( @args[:port], baud, data_bits, stop_bits, parity )
63
+
64
+ got = login()
65
+ yield self if block_given?
66
+ trans_start_netconf( got )
67
+
68
+ self
69
+ end
70
+
71
+ def trans_hello
72
+ hello_str = trans_receive()
73
+ so_xml = hello_str.index("\n") + 1
74
+ hello_str.slice!(0, so_xml)
75
+ hello_str
76
+ end
77
+
78
+ def trans_close
79
+ @trans.write Netconf::RPC::MSG_CLOSE_SESSION
80
+ @trans.close
81
+ end
82
+
83
+ def trans_send( cmd_str )
84
+ @trans.write( cmd_str )
85
+ @trans.fsync
86
+ end
87
+
88
+ def trans_receive
89
+ got = waitfor( Netconf::RPC::MSG_END_RE )
90
+ msg_end = got.rindex( Netconf::RPC::MSG_END )
91
+ got[msg_end .. -1] = ''
92
+ got
93
+ end
94
+
95
+ def puts( str = nil )
96
+ @trans.puts str
97
+ @trans.fsync
98
+ end
99
+
100
+ def waitfor( this_re = nil )
101
+ on_re = this_re || @args[:prompt]
102
+
103
+ time_out = @trans_timeout
104
+ wait_io = @trans_waitio
105
+
106
+ time_out = nil if time_out == false
107
+ done = false
108
+ rx_buf = ''
109
+
110
+ until( rx_buf.match( on_re ) and not IO::select( [@trans], nil, nil, wait_io ) )
111
+
112
+ unless IO::select( [@trans], nil, nil, time_out )
113
+ raise TimeoutError, "Netconf IO timed out while waiting for more data"
114
+ end
115
+
116
+ begin
117
+
118
+ rx_some = @trans.readpartial( DEFAULT_RDBLKSZ )
119
+ rx_buf += rx_some
120
+ break if rx_buf.match( on_re )
121
+
122
+ rescue EOFError # End of file reached
123
+ rx_buf = nil if rx_buf == ''
124
+ break # out of outer 'until' loop
125
+ end
126
+
127
+ end
128
+ rx_buf
129
+ end
130
+
131
+ end # class: Serial
132
+ end # module: Netconf
@@ -0,0 +1,77 @@
1
+ require 'net/ssh'
2
+
3
+ module Netconf
4
+ class SSH < Netconf::Transport
5
+
6
+ NETCONF_PORT = 830
7
+ NETCONF_SUBSYSTEM = 'netconf'
8
+
9
+ def initialize( args_h, &block )
10
+ @args = args_h.clone
11
+ @trans = Hash.new
12
+
13
+ super( &block )
14
+ end
15
+
16
+ def trans_open( &block )
17
+ # open a connection to the NETCONF subsystem
18
+ start_args = Hash.new
19
+ start_args[:password] ||= @args[:password]
20
+ start_args[:passphrase] = @args[:passphrase] || nil
21
+ start_args[:port] = @args[:port] || NETCONF_PORT
22
+
23
+ @trans[:conn] = Net::SSH.start( @args[:target], @args[:username], start_args )
24
+ @trans[:chan] = @trans[:conn].open_channel{ |ch| ch.subsystem( NETCONF_SUBSYSTEM ) }
25
+ end
26
+
27
+ def trans_close
28
+ @trans[:chan].close if @trans[:chan]
29
+ @trans[:conn].close if @trans[:conn]
30
+ end
31
+
32
+ def trans_receive
33
+ @trans[:rx_buf] = ''
34
+ @trans[:more] = true
35
+
36
+ # collect the response data as it comes back ...
37
+ # the "on" functions must be set before calling
38
+ # the #loop method
39
+
40
+ @trans[:chan].on_data do |ch, data|
41
+ if data.include?( RPC::MSG_END )
42
+ data.slice!( RPC::MSG_END )
43
+ @trans[:rx_buf] << data unless data.empty?
44
+ @trans[:more] = false
45
+ else
46
+ @trans[:rx_buf] << data
47
+ end
48
+ end
49
+
50
+ # ... if there are errors ...
51
+ @trans[:chan].on_extended_data do |ch, type, data|
52
+ @trans[:rx_err] = data
53
+ @trans[:more] = false
54
+ end
55
+
56
+ # the #loop method is what actually performs
57
+ # ssh event processing ...
58
+
59
+ @trans[:conn].loop { @trans[:more] }
60
+
61
+ return @trans[:rx_buf]
62
+ end
63
+
64
+ def trans_send( cmd_str )
65
+ @trans[:chan].send_data( cmd_str )
66
+ end
67
+
68
+ # accessor to create an Net::SCP object so the caller can perform
69
+ # secure-copy operations (see Net::SCP) for details
70
+ def scp
71
+ @scp ||= Net::SCP.start( @args[:target], @args[:username], :password => @args[:password] )
72
+ end
73
+
74
+ end # class: SSH
75
+ end #module: Netconf
76
+
77
+ require 'net/netconf/ssh'
@@ -0,0 +1,58 @@
1
+ require 'net/telnet'
2
+
3
+ module Netconf
4
+
5
+ class Telnet < Netconf::Transport
6
+
7
+ def initialize( args, trans_args = nil, &block )
8
+ os_type = args[:os_type] || Netconf::DEFAULT_OS_TYPE
9
+ @args = args.clone
10
+
11
+ # extend this instance with the capabilities of the specific console
12
+ # type; it needs to define #login and #start_netconf session
13
+ begin
14
+ extend Netconf::TransTelnet::const_get( os_type )
15
+ rescue NameError
16
+ # no extensions available ...
17
+ end
18
+
19
+ my_trans_args = {}
20
+ my_trans_args["Host"] = @args[:target]
21
+ my_trans_args["Port"] = @args[:port] if @args[:port]
22
+
23
+ @trans = Net::Telnet.new( my_trans_args )
24
+
25
+ @trans_timeout = @args[:timeout] || Netconf::DEFAULT_TIMEOUT
26
+ @trans_waitio = @args[:waitio] || Netconf::DEFAULT_WAITIO
27
+
28
+ super( &block )
29
+ end
30
+
31
+ def trans_open( &block )
32
+ trans_login()
33
+ trans_start_netconf()
34
+ self
35
+ end
36
+
37
+ def trans_hello
38
+ hello_str = trans_receive()
39
+ so_xml = hello_str.index("\n") + 1 # skip over the last issued command
40
+ hello_str[so_xml .. -1]
41
+ end
42
+
43
+ def trans_close
44
+ @trans.write Netconf::RPC::MSG_CLOSE_SESSION
45
+ @trans.close
46
+ end
47
+
48
+ def trans_send( cmd_str )
49
+ @trans.write( cmd_str )
50
+ end
51
+
52
+ def trans_receive
53
+ rsp = @trans.waitfor( Netconf::RPC::MSG_END_RE )
54
+ rsp.chomp!( Netconf::RPC::MSG_END + "\n" )
55
+ end
56
+
57
+ end # class: Serial
58
+ end # module: Netconf
@@ -0,0 +1,113 @@
1
+ ## -----------------------------------------------------------------------
2
+ ## This file contains the Netconf::Transport parent class definition.
3
+ ## All other transports, i.e. "ssh", "serial", "telnet" use this parent
4
+ ## class to define their transport specific methods:
5
+ ##
6
+ ## trans_open: open the transport connection
7
+ ## trans_close: close the transport connection
8
+ ## trans_send: send XML command (String) via transport
9
+ ## trans_receive: receive XML response (String) via transport
10
+ ##
11
+ ## -----------------------------------------------------------------------
12
+
13
+ module Netconf
14
+ class Transport
15
+
16
+ attr_reader :rpc, :state, :session_id, :capabilities
17
+ attr_writer :timeout, :waitio
18
+
19
+ def initialize( &block )
20
+
21
+ @state = :NETCONF_CLOSED
22
+ @os_type = @args[:os_type] || Netconf::DEFAULT_OS_TYPE
23
+
24
+ @rpc = Netconf::RPC::Executor.new( self, @os_type )
25
+
26
+ if block_given?
27
+ open( &block = nil ) # do not pass this block to open()
28
+ yield self
29
+ close()
30
+ end
31
+
32
+ end # initialize
33
+
34
+ def open( &block ) # :yield: specialized transport open, generally not used
35
+
36
+ raise Netconf::StateError if @state == :NETCONF_OPEN
37
+
38
+ # block is used to deal with special open processing ...
39
+ # this is *NOT* the block passed to initialize()
40
+ raise Netconf::OpenError unless trans_open( &block )
41
+ @state = :NETCONF_OPEN
42
+
43
+ hello_rsp = Nokogiri::XML( trans_hello() )
44
+
45
+ @capabilities = hello_rsp.xpath('//capability').map{ |c| c.text }
46
+ @session_id = hello_rsp.xpath('//session-id').text
47
+ self
48
+ end
49
+
50
+ def trans_hello
51
+ trans_receive()
52
+ end
53
+
54
+ def has_capability?( capability )
55
+ @capabilities.select{|c| c.include? capability }.pop
56
+ # note: the caller could also simply use #grep on @capabilities
57
+ end
58
+
59
+ def close
60
+ raise Netconf::StateError unless @state == :NETCONF_OPEN
61
+ trans_close()
62
+ @state = :NETCONF_CLOSED
63
+ self
64
+ end
65
+
66
+ # string in; string out
67
+ def send_and_receive( cmd_str )
68
+ trans_send( cmd_str )
69
+ trans_receive()
70
+ end
71
+
72
+ def rpc_exec( cmd_nx )
73
+ raise Netconf::StateError unless @state == :NETCONF_OPEN
74
+
75
+ # send the XML command through the transport and
76
+ # receive the response; then covert it to a Nokogiri XML
77
+ # object so we can process it.
78
+
79
+ rsp_nx = Nokogiri::XML( send_and_receive( cmd_nx.to_xml ))
80
+
81
+ # the following removes only the default namespace (xmlns)
82
+ # definitions from the document. This is an alternative
83
+ # to using #remove_namespaces! which would remove everything
84
+ # including vendor specific namespaces. So this approach is a
85
+ # nice "compromise" ... just don't know what it does
86
+ # performance-wise on large datasets.
87
+
88
+ rsp_nx.traverse{ |n| n.namespace = nil }
89
+
90
+ # set the response context to the root node; <rpc-reply>
91
+
92
+ rsp_nx = rsp_nx.root
93
+
94
+ # the <rpc-error> could either be at the toplevel or
95
+ # as a child element of toplevel.
96
+
97
+ if rsp_nx.xpath( "rpc-error|*/rpc-error" )[0]
98
+ exception = Netconf::RPC.get_exception( cmd_nx )
99
+ raise exception.new( self, cmd_nx, rsp_nx )
100
+ end
101
+
102
+ # return the XML with context at toplevel element; i.e.
103
+ # after the <rpc-reply> element
104
+ # @@@/JLS: might this be <ok> ? isn't for Junos, but need to check
105
+ # @@@/JLS: the generic case.
106
+
107
+ rsp_nx.first_element_child
108
+
109
+ end
110
+
111
+
112
+ end #--class: Transport
113
+ end #--module: Netconf