netconf 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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