hrr_rb_netconf 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +28 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +32 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +6 -0
  7. data/LICENSE +201 -0
  8. data/README.md +220 -0
  9. data/Rakefile +6 -0
  10. data/demo/server.rb +54 -0
  11. data/demo/server_over_ssh.rb +97 -0
  12. data/demo/server_with_session-oriented-database.rb +134 -0
  13. data/demo/server_with_sessionless-database.rb +81 -0
  14. data/hrr_rb_netconf.gemspec +28 -0
  15. data/lib/hrr_rb_netconf.rb +10 -0
  16. data/lib/hrr_rb_netconf/logger.rb +56 -0
  17. data/lib/hrr_rb_netconf/server.rb +109 -0
  18. data/lib/hrr_rb_netconf/server/capabilities.rb +75 -0
  19. data/lib/hrr_rb_netconf/server/capability.rb +206 -0
  20. data/lib/hrr_rb_netconf/server/capability/base_1_0.rb +183 -0
  21. data/lib/hrr_rb_netconf/server/capability/base_1_1.rb +247 -0
  22. data/lib/hrr_rb_netconf/server/capability/candidate_1_0.rb +34 -0
  23. data/lib/hrr_rb_netconf/server/capability/confirmed_commit_1_0.rb +24 -0
  24. data/lib/hrr_rb_netconf/server/capability/confirmed_commit_1_1.rb +24 -0
  25. data/lib/hrr_rb_netconf/server/capability/rollback_on_error_1_0.rb +16 -0
  26. data/lib/hrr_rb_netconf/server/capability/startup_1_0.rb +22 -0
  27. data/lib/hrr_rb_netconf/server/capability/url_1_0.rb +23 -0
  28. data/lib/hrr_rb_netconf/server/capability/validate_1_0.rb +25 -0
  29. data/lib/hrr_rb_netconf/server/capability/validate_1_1.rb +25 -0
  30. data/lib/hrr_rb_netconf/server/capability/writable_running_1_0.rb +17 -0
  31. data/lib/hrr_rb_netconf/server/capability/xpath_1_0.rb +14 -0
  32. data/lib/hrr_rb_netconf/server/datastore.rb +30 -0
  33. data/lib/hrr_rb_netconf/server/datastore/oper_handler.rb +29 -0
  34. data/lib/hrr_rb_netconf/server/datastore/session.rb +52 -0
  35. data/lib/hrr_rb_netconf/server/error.rb +52 -0
  36. data/lib/hrr_rb_netconf/server/error/access_denied.rb +19 -0
  37. data/lib/hrr_rb_netconf/server/error/bad_attribute.rb +20 -0
  38. data/lib/hrr_rb_netconf/server/error/bad_element.rb +20 -0
  39. data/lib/hrr_rb_netconf/server/error/data_exists.rb +19 -0
  40. data/lib/hrr_rb_netconf/server/error/data_missing.rb +19 -0
  41. data/lib/hrr_rb_netconf/server/error/in_use.rb +19 -0
  42. data/lib/hrr_rb_netconf/server/error/invalid_value.rb +19 -0
  43. data/lib/hrr_rb_netconf/server/error/lock_denied.rb +19 -0
  44. data/lib/hrr_rb_netconf/server/error/malformed_message.rb +19 -0
  45. data/lib/hrr_rb_netconf/server/error/missing_attribute.rb +19 -0
  46. data/lib/hrr_rb_netconf/server/error/missing_element.rb +19 -0
  47. data/lib/hrr_rb_netconf/server/error/operation_failed.rb +19 -0
  48. data/lib/hrr_rb_netconf/server/error/operation_not_supported.rb +19 -0
  49. data/lib/hrr_rb_netconf/server/error/partial_operation.rb +19 -0
  50. data/lib/hrr_rb_netconf/server/error/resource_denied.rb +19 -0
  51. data/lib/hrr_rb_netconf/server/error/rollback_failed.rb +19 -0
  52. data/lib/hrr_rb_netconf/server/error/rpc_errorable.rb +138 -0
  53. data/lib/hrr_rb_netconf/server/error/too_big.rb +19 -0
  54. data/lib/hrr_rb_netconf/server/error/unknown_attribute.rb +19 -0
  55. data/lib/hrr_rb_netconf/server/error/unknown_element.rb +19 -0
  56. data/lib/hrr_rb_netconf/server/error/unknown_namespace.rb +19 -0
  57. data/lib/hrr_rb_netconf/server/errors.rb +28 -0
  58. data/lib/hrr_rb_netconf/server/filter.rb +48 -0
  59. data/lib/hrr_rb_netconf/server/filter/subtree.rb +135 -0
  60. data/lib/hrr_rb_netconf/server/filter/xpath.rb +59 -0
  61. data/lib/hrr_rb_netconf/server/model.rb +123 -0
  62. data/lib/hrr_rb_netconf/server/model/node.rb +19 -0
  63. data/lib/hrr_rb_netconf/server/operation.rb +92 -0
  64. data/lib/hrr_rb_netconf/server/session.rb +177 -0
  65. data/lib/hrr_rb_netconf/version.rb +6 -0
  66. metadata +149 -0
@@ -0,0 +1,59 @@
1
+ # coding: utf-8
2
+ # vim: et ts=2 sw=2
3
+
4
+ require 'rexml/document'
5
+
6
+ module HrrRbNetconf
7
+ class Server
8
+ class Filter
9
+ class Xpath < Filter
10
+ TYPE = 'xpath'
11
+
12
+ class << self
13
+ def filter raw_output_e, filter_e
14
+ xpath = filter_e.attributes['select']
15
+ unless xpath
16
+ raise Error['missing-attribute'].new('protocol', 'error', info: {'bad-attribute' => 'select', 'bad-element' => 'filter'})
17
+ end
18
+ raw_output_xml_doc = REXML::Document.new
19
+ raw_output_xml_doc.add raw_output_e.deep_clone
20
+ selected_element_xpaths = []
21
+ output_xml_doc = REXML::Document.new
22
+ raw_output_xml_doc.each_element(xpath){ |e|
23
+ ctx_output_e = add_ancestors_recursively selected_element_xpaths, output_xml_doc, e
24
+ e.children.each{ |c|
25
+ case c
26
+ when REXML::Parent
27
+ ctx_output_e.add c.deep_clone
28
+ else
29
+ ctx_output_e.add c.clone
30
+ end
31
+ }
32
+ }
33
+ output_xml_doc.root
34
+ end
35
+
36
+ def add_elem selected_element_xpaths, output_e, raw_output_e
37
+ xpath, elem = selected_element_xpaths.find{ |xpath, elem| xpath == raw_output_e.xpath }
38
+ unless xpath
39
+ child_output_e = output_e.add raw_output_e.clone
40
+ selected_element_xpaths.push [raw_output_e.xpath, child_output_e]
41
+ child_output_e
42
+ else
43
+ elem
44
+ end
45
+ end
46
+
47
+ def add_ancestors_recursively selected_element_xpaths, output_e, raw_output_e
48
+ if raw_output_e == raw_output_e.root_node
49
+ output_e
50
+ else
51
+ parent_e = add_ancestors_recursively selected_element_xpaths, output_e, raw_output_e.parent
52
+ add_elem selected_element_xpaths, parent_e, raw_output_e
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,123 @@
1
+ # coding: utf-8
2
+ # vim: et ts=2 sw=2
3
+
4
+ require 'rexml/document'
5
+ require 'hrr_rb_netconf/server/error'
6
+ require 'hrr_rb_netconf/server/model/node'
7
+
8
+ module HrrRbNetconf
9
+ class Server
10
+ class Model
11
+ def initialize operation
12
+ @operation = operation
13
+ @tree = Node.new nil, operation, 'root', {}
14
+ end
15
+
16
+ def add_recursively capability, node, path, stmt, options
17
+ name = path.shift
18
+ case path.size
19
+ when 0
20
+ node.children.push Node.new capability, name, stmt, options
21
+ else
22
+ child_node = node.children.find{|n| name == n.name}
23
+ add_recursively capability, child_node, path, stmt, options
24
+ end
25
+ end
26
+
27
+ def add capability, path, stmt, options
28
+ if path.size > 0
29
+ add_recursively capability, @tree, path.dup, stmt, options
30
+ end
31
+ end
32
+
33
+ def validate_recursively node, xml_e, parent_xml_e: nil, validated: []
34
+ case node.stmt
35
+ when 'root', 'container'
36
+ case xml_e
37
+ when nil
38
+ true
39
+ else
40
+ node.children.all?{ |c|
41
+ case c.stmt
42
+ when 'container'
43
+ validated.push c.name
44
+ validate_recursively c, xml_e.elements[c.name]
45
+ when 'leaf'
46
+ validated.push c.name
47
+ if xml_e.elements[c.name].nil? && c.options['default'].nil?
48
+ if c.options['validation'].nil?
49
+ true
50
+ else
51
+ raise Error['operation-failed'].new('application', 'error', message: 'Not implemented')
52
+ end
53
+ else
54
+ validate_recursively c, xml_e.elements[c.name], parent_xml_e: xml_e
55
+ end
56
+ when 'choice'
57
+ validate_recursively c, xml_e, validated: validated
58
+ else
59
+ raise Error['unknown-element'].new('application', 'error', info: {'bad-element' => "#{c.name}: #{c.stmt}"}, message: 'Not implemented')
60
+ end
61
+ }
62
+ end && (xml_e.elements.to_a.map{|e| e.name} - validated).empty?
63
+ when 'leaf'
64
+ case node.options['type']
65
+ when 'empty'
66
+ xml_e != nil && xml_e.has_text?.!
67
+ when 'enumeration'
68
+ if xml_e == nil && node.options['default']
69
+ parent_xml_e.add_element(node.name).text = node.options['default']
70
+ else
71
+ xml_e != nil && node.options['enum'].include?(xml_e.text)
72
+ end
73
+ when 'anyxml'
74
+ xml_e != nil && (REXML::Document.new(xml_e.text) rescue false)
75
+ when 'inet:uri'
76
+ xml_e != nil
77
+ raise Error['unknown-element'].new('application', 'error', info: {'bad-element' => "#{node.name}: #{node.stmt}"}, message: 'Not implemented: type inet:uri')
78
+ when 'integer'
79
+ if xml_e == nil && node.options['default']
80
+ parent_xml_e.add_element(node.name).text = node.options['default']
81
+ else
82
+ value = (Integer(xml_e.text) rescue false)
83
+ if node.options['range']
84
+ min, max = node.options['range']
85
+ xml_e != nil && value && min <= value && value <= max
86
+ else
87
+ xml_e != nil && value
88
+ end
89
+ end
90
+ when 'string'
91
+ if xml_e == nil && node.options['default']
92
+ parent_xml_e.add_element(node.name).text = node.options['default']
93
+ else
94
+ if node.options['validation'].nil?
95
+ xml_e != nil && xml_e.has_text?
96
+ else
97
+ xml_e != nil && xml_e.has_text? && node.options['validation'].call(node.capability, xml_e)
98
+ end
99
+ end
100
+ end
101
+ when 'choice'
102
+ if node.options['mandatory']
103
+ node.children.any?{ |c|
104
+ validated.push c.name
105
+ validate_recursively c, xml_e.elements[c.name]
106
+ }
107
+ else
108
+ node.children.empty? || node.children.any?{ |c|
109
+ validated.push c.name
110
+ validate_recursively c, xml_e.elements[c.name]
111
+ }
112
+ end
113
+ else
114
+ raise Error['unknown-element'].new('application', 'error', info: {'bad-element' => "#{c.name}: #{c.stmt}"}, message: 'Not implemented')
115
+ end
116
+ end
117
+
118
+ def validate input_e
119
+ validate_recursively @tree, input_e
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,19 @@
1
+ # coding: utf-8
2
+ # vim: et ts=2 sw=2
3
+
4
+ module HrrRbNetconf
5
+ class Server
6
+ class Model
7
+ class Node
8
+ attr_reader :capability, :name, :stmt, :options, :children
9
+ def initialize capability, name, stmt, options
10
+ @capability = capability
11
+ @name = name
12
+ @stmt = stmt
13
+ @options = options
14
+ @children = Array.new
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,92 @@
1
+ # coding: utf-8
2
+ # vim: et ts=2 sw=2
3
+
4
+ require 'hrr_rb_netconf/logger'
5
+ require 'hrr_rb_netconf/server/model'
6
+ require 'hrr_rb_netconf/server/filter'
7
+
8
+ module HrrRbNetconf
9
+ class Server
10
+ class Operation
11
+ def initialize session, capabilities, datastore_session, strict_capabilities
12
+ @logger = Logger.new self.class.name
13
+ @session = session
14
+ @capabilities = capabilities
15
+ @datastore_session = datastore_session
16
+ @strict_capabilities = strict_capabilities
17
+ @models = Hash.new
18
+ @oper_procs = Hash.new
19
+
20
+ load_capabilities
21
+ end
22
+
23
+ def load_capabilities
24
+ @capabilities.each_loadable{ |c|
25
+ @logger.debug { "Load capability: #{c.id}" }
26
+ c.oper_procs.each{ |k, v|
27
+ @oper_procs[k] = v
28
+ }
29
+ if @strict_capabilities
30
+ c.models.each{ |m|
31
+ oper_name, path, stmt, options = m
32
+ @models[oper_name] ||= Model.new oper_name
33
+ @models[oper_name].add c, path, stmt, options
34
+ }
35
+ end
36
+ }
37
+ end
38
+
39
+ def validate input_e
40
+ oper_name = input_e.name
41
+ model = @models[oper_name]
42
+ if model
43
+ model.validate input_e
44
+ else
45
+ false
46
+ end
47
+ end
48
+
49
+ def run xml_doc
50
+ unless xml_doc.root.name == 'rpc'
51
+ @logger.error { "Invalid root tag: must be rpc, but got #{xml_doc.root.name}" }
52
+ raise Error['operation-not-supported'].new('protocol', 'error')
53
+ end
54
+
55
+ message_id = xml_doc.attributes['message-id']
56
+ unless message_id
57
+ raise Error['missing-attribute'].new('rpc', 'error', info: {'bad-attribute' => 'message-id', 'bad-element' => 'rpc'})
58
+ end
59
+
60
+ input_e = xml_doc.elements[1]
61
+
62
+ unless @oper_procs.has_key? input_e.name
63
+ raise Error['operation-not-supported'].new('protocol', 'error')
64
+ end
65
+
66
+ if @strict_capabilities
67
+ unless validate input_e
68
+ raise Error['operation-not-supported'].new('application', 'error')
69
+ end
70
+ end
71
+
72
+ raw_output = @oper_procs[input_e.name].call(@session, @datastore_session, input_e)
73
+
74
+ raw_output_e = case raw_output
75
+ when String
76
+ REXML::Document.new(raw_output, {:ignore_whitespace_nodes => :all}).root
77
+ when REXML::Document
78
+ raw_output.root
79
+ when REXML::Element
80
+ raw_output
81
+ else
82
+ raise "Unexpected output: #{raw_output.inspect}"
83
+ end
84
+ output_e = Filter.filter(raw_output_e, input_e)
85
+ rpc_reply_e = xml_doc.clone
86
+ rpc_reply_e.name = "rpc-reply"
87
+ rpc_reply_e.add output_e
88
+ rpc_reply_e
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,177 @@
1
+ # coding: utf-8
2
+ # vim: et ts=2 sw=2
3
+
4
+ require 'rexml/document'
5
+ require 'hrr_rb_netconf/logger'
6
+ require 'hrr_rb_netconf/server/capability'
7
+ require 'hrr_rb_netconf/server/operation'
8
+
9
+ module HrrRbNetconf
10
+ class Server
11
+ class Session
12
+ attr_reader :session_id
13
+
14
+ def initialize server, capabilities, datastore, session_id, io, strict_capabilities
15
+ @logger = Logger.new self.class.name
16
+ @server = server
17
+ @local_capabilities = capabilities
18
+ @remote_capabilities = Array.new
19
+ @datastore = datastore
20
+ @session_id = session_id
21
+ @io_r, @io_w = case io
22
+ when IO
23
+ [io, io]
24
+ when Array
25
+ [io[0], io[1]]
26
+ else
27
+ raise ArgumentError, "io must be an instance of IO or Array"
28
+ end
29
+ @strict_capabilities = strict_capabilities
30
+ @closed = false
31
+ end
32
+
33
+ def start
34
+ begin
35
+ exchange_hello
36
+ negotiate_capabilities
37
+ initialize_sender_and_receiver
38
+ operation_loop
39
+ rescue
40
+ raise
41
+ ensure
42
+ close
43
+ end
44
+ end
45
+
46
+ def close
47
+ @logger.info { "Being closed" }
48
+ @closed = true
49
+ begin
50
+ @io_r.close_read
51
+ rescue
52
+ end
53
+ end
54
+
55
+ def closed?
56
+ @closed
57
+ end
58
+
59
+ def exchange_hello
60
+ send_hello
61
+ receive_hello
62
+ end
63
+
64
+ def send_hello
65
+ @logger.info { "Local capabilities: #{@local_capabilities}" }
66
+ xml_doc = REXML::Document.new
67
+ hello_e = xml_doc.add_element 'hello'
68
+ hello_e.add_namespace('urn:ietf:params:xml:ns:netconf:base:1.0')
69
+ capabilities_e = hello_e.add_element 'capabilities'
70
+ @local_capabilities.list_loadable.each{ |c|
71
+ capability_e = capabilities_e.add_element 'capability'
72
+ capability_e.text = c
73
+ }
74
+ session_id_e = hello_e.add_element 'session-id'
75
+ session_id_e.text = @session_id.to_s
76
+
77
+ buf = String.new
78
+ formatter = REXML::Formatters::Pretty.new(2)
79
+ formatter.compact = true
80
+ formatter.write(xml_doc, buf)
81
+ @logger.debug { "Sending hello message: #{buf.inspect}" }
82
+ @io_w.write "#{buf}\n]]>]]>\n"
83
+ end
84
+
85
+ def receive_hello
86
+ buf = String.new
87
+ loop do
88
+ buf += @io_r.read(1)
89
+ if buf[-6..-1] == ']]>]]>'
90
+ break
91
+ end
92
+ end
93
+ @logger.debug { "Received hello message: #{buf[0..-7].inspect}" }
94
+ remote_capabilities_xml_doc = REXML::Document.new(buf[0..-7], {:ignore_whitespace_nodes => :all})
95
+ remote_capabilities_xml_doc.each_element('/hello/capabilities/capability'){ |c| @remote_capabilities.push c.text }
96
+ @logger.info { "Remote capabilities: #{@remote_capabilities}" }
97
+ end
98
+
99
+ def negotiate_capabilities
100
+ @capabilities = @local_capabilities.negotiate @remote_capabilities
101
+ @logger.info { "Negotiated capabilities: #{@capabilities.list_loadable}" }
102
+ unless @capabilities.list_loadable.any?{ |c| /^urn:ietf:params:netconf:base:\d+\.\d+$/ =~ c }
103
+ @logger.error { "No base NETCONF capability negotiated" }
104
+ raise "No base NETCONF capability negotiated"
105
+ end
106
+ end
107
+
108
+ def initialize_sender_and_receiver
109
+ base_capability = @capabilities.list_loadable.select{ |c| /^urn:ietf:params:netconf:base:\d+\.\d+$/ =~ c }.sort.last
110
+ @logger.info { "Base NETCONF capability: #{base_capability}" }
111
+ @sender = Capability[base_capability]::Sender.new @io_w
112
+ @receiver = Capability[base_capability]::Receiver.new @io_r
113
+ end
114
+
115
+ def operation_loop
116
+ datastore_session = @datastore.new_session self
117
+ operation = Operation.new self, @capabilities, datastore_session, @strict_capabilities
118
+
119
+ begin
120
+ loop do
121
+ if closed?
122
+ break
123
+ end
124
+
125
+ begin
126
+ begin
127
+ received_message = @receiver.receive_message
128
+ break unless received_message
129
+ rpc_reply_e = operation.run received_message
130
+ rescue Error => e
131
+ if received_message
132
+ rpc_reply_e = received_message.clone
133
+ rpc_reply_e.name = "rpc-reply"
134
+ else
135
+ rpc_reply_e = REXML::Element.new("rpc-reply")
136
+ rpc_reply_e.add_namespace("urn:ietf:params:xml:ns:netconf:base:1.0")
137
+ end
138
+ rpc_reply_e.add e.to_rpc_error
139
+ end
140
+
141
+ begin
142
+ @sender.send_message rpc_reply_e
143
+ rescue IOError
144
+ break
145
+ end
146
+ rescue => e
147
+ @logger.error { e.message }
148
+ raise
149
+ end
150
+ end
151
+ ensure
152
+ begin
153
+ datastore_session.close
154
+ rescue
155
+ end
156
+ begin
157
+ @io_w.close_write
158
+ rescue
159
+ end
160
+ end
161
+ @logger.info { "Exit operation_loop" }
162
+ end
163
+
164
+ def close_other session_id
165
+ @server.close_session session_id
166
+ end
167
+
168
+ def lock target
169
+ @server.lock target, @session_id
170
+ end
171
+
172
+ def unlock target
173
+ @server.unlock target, @session_id
174
+ end
175
+ end
176
+ end
177
+ end