hrr_rb_netconf 0.1.0

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.
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