blob: 2b1fb6557c368ae7a56440057088ce995bc0cf30 [file] [log] [blame]
Khen Nursimulu3869d8d2016-11-28 20:44:28 -05001#
2# Copyright 2016 the original author or authors.
3#
4# Code adapted from https://github.com/choppsv1/netconf
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18from __future__ import absolute_import, division, unicode_literals, \
19 print_function, nested_scopes
20import structlog
21import io
22from lxml import etree
23from lxml.builder import E
24import netconf.error as ncerror
25from netconf import NSMAP, qmap
26from utils import elm
27from twisted.internet.defer import inlineCallbacks, returnValue, Deferred
28
29log = structlog.get_logger()
30
31class NetconfProtocolError(Exception): pass
32
33
34NC_BASE_10 = "urn:ietf:params:netconf:base:1.0"
35NC_BASE_11 = "urn:ietf:params:netconf:base:1.1"
36XML_HEADER = """<?xml version="1.0" encoding="utf-8"?>"""
37
38
39class NetconfMethods(object):
40 """This is an abstract class that is used to document the server methods functionality
41
42 The server return not-implemented if the method is not found in the methods object,
43 so feel free to use duck-typing here (i.e., no need to inherit)
44 """
45
46 def nc_append_capabilities(self, capabilities): # pylint: disable=W0613
47 """The server should append any capabilities it supports to capabilities"""
48 return
49
50 def rpc_get(self, session, rpc, filter_or_none): # pylint: disable=W0613
51 """Passed the filter element or None if not present"""
52 raise ncerror.RPCSvrErrNotImpl(rpc)
53
54 def rpc_get_config(self, session, rpc, source_elm,
55 filter_or_none): # pylint: disable=W0613
56 """Passed the source element"""
57 raise ncerror.RPCSvrErrNotImpl(rpc)
58
59 # TODO: The API WILL CHANGE consider unfinished
60 def rpc_copy_config(self, unused_session, rpc, *unused_params):
61 raise ncerror.RPCSvrErrNotImpl(rpc)
62
63 # TODO: The API WILL CHANGE consider unfinished
64 def rpc_delete_config(self, unused_session, rpc, *unused_params):
65 raise ncerror.RPCSvrErrNotImpl(rpc)
66
67 # TODO: The API WILL CHANGE consider unfinished
68 def rpc_edit_config(self, unused_session, rpc, *unused_params):
69 raise ncerror.RPCSvrErrNotImpl(rpc)
70
71 # TODO: The API WILL CHANGE consider unfinished
72 def rpc_lock(self, unused_session, rpc, *unused_params):
73 raise ncerror.RPCSvrErrNotImpl(rpc)
74
75 # TODO: The API WILL CHANGE consider unfinished
76 def rpc_unlock(self, unused_session, rpc, *unused_params):
77 raise ncerror.RPCSvrErrNotImpl(rpc)
78
79
80class NetconfMethods(NetconfMethods):
81 def rpc_get(self, unused_session, rpc, *unused_params):
82 return etree.Element("ok")
83
84 def rpc_get_config(self, unused_session, rpc, *unused_params):
85 return etree.Element("ok")
86
87 def rpc_namespaced(self, unused_session, rpc, *unused_params):
88 return etree.Element("ok")
89
90
91class NetconfProtocolHandler:
92 def __init__(self, nc_server, nc_conn, grpc_stub):
93 self.started = True
94 self.conn = nc_conn
95 self.nc_server = nc_server
96 self.grpc_stub = grpc_stub
97 self.methods = NetconfMethods()
98 self.new_framing = False
99 self.capabilities = set()
100 self.session_id = 1
101 self.session_open = False
102 self.exiting = False
103 self.connected = Deferred()
104 self.connected.addCallback(self.nc_server.client_disconnected,
105 self, None)
106
107 def send_message(self, msg):
108 self.conn.send_msg(XML_HEADER + msg, self.new_framing)
109
110 def receive_message(self):
111 return self.conn.receive_msg_any(self.new_framing)
112
113 def allocate_session_id(self):
114 sid = self.session_id
115 self.session_id += 1
116 return sid
117
118 def send_hello(self, caplist, session_id=None):
119 log.debug('starting', sessionId=session_id)
120 msg = elm("hello", attrib={'xmlns': NSMAP['nc']})
121 caps = E.capabilities(*[E.capability(x) for x in caplist])
122 if session_id is not None:
123 assert hasattr(self, "methods")
124 self.methods.nc_append_capabilities(
125 caps) # pylint: disable=E1101
126 msg.append(caps)
127
128 if session_id is not None:
129 msg.append(E("session-id", str(session_id)))
130 msg = etree.tostring(msg)
131 log.info("Sending HELLO", msg=msg)
132 msg = msg.decode('utf-8')
133 self.send_message(msg)
134
135 def send_rpc_reply(self, rpc_reply, origmsg):
136 reply = etree.Element(qmap('nc') + "rpc-reply", attrib=origmsg.attrib,
137 nsmap=origmsg.nsmap)
138 try:
139 rpc_reply.getchildren # pylint: disable=W0104
140 reply.append(rpc_reply)
141 except AttributeError:
142 reply.extend(rpc_reply)
143 ucode = etree.tounicode(reply, pretty_print=True)
144 log.debug("RPC-Reply", reply=ucode)
145 self.send_message(ucode)
146
147 @inlineCallbacks
148 def open_session(self):
149 # The transport should be connected at this point.
150 try:
151 # Send hello message.
152 yield self.send_hello((NC_BASE_10, NC_BASE_11), self.session_id)
153
154 # Get reply
155 reply = yield self.receive_message()
156 log.info("reply-received", reply=reply)
157
158 # Parse reply
159 tree = etree.parse(io.BytesIO(reply.encode('utf-8')))
160 root = tree.getroot()
161 caps = root.xpath("//nc:hello/nc:capabilities/nc:capability",
162 namespaces=NSMAP)
163
164 # Store capabilities
165 for cap in caps:
166 self.capabilities.add(cap.text)
167
168 if NC_BASE_11 in self.capabilities:
169 self.new_framing = True
170 elif NC_BASE_10 not in self.capabilities:
171 raise SessionError(
172 "Server doesn't implement 1.0 or 1.1 of netconf")
173
174 self.session_open = True
175
176 log.info('session-opened', session_id=self.session_id,
177 framing="1.1" if self.new_framing else "1.0")
178
179 except Exception as e:
180 self.stop(repr(e))
181 raise
182
183 @inlineCallbacks
184 def start(self):
185 log.info('starting')
186
187 try:
188 yield self.open_session()
189 while True:
190 if not self.session_open:
191 break;
192
193 msg = yield self.receive_message()
194 self.handle_request(msg)
195 except Exception as e:
196 log.exception('exception', e=e)
197 self.stop(repr(e))
198
199 log.info('shutdown')
200 returnValue(self)
201
202 def handle_request(self, msg):
203 if not self.session_open:
204 return
205
206 # Any error with XML encoding here is going to cause a session close
207 # TODO: Return a malformed message.
208 try:
209 tree = etree.parse(io.BytesIO(msg.encode('utf-8')))
210 if not tree:
211 raise ncerror.SessionError(msg, "Invalid XML from client.")
212 except etree.XMLSyntaxError:
213 log.error("Closing-session-malformed-message", msg=msg)
214 raise ncerror.SessionError(msg, "Invalid XML from client.")
215
216 rpcs = tree.xpath("/nc:rpc", namespaces=NSMAP)
217 if not rpcs:
218 raise ncerror.SessionError(msg, "No rpc found")
219
220 # A message can have multiple rpc requests
221 for rpc in rpcs:
222 try:
223 msg_id = rpc.get('message-id')
224 log.info("Received-rpc-message-id", msg_id=msg_id)
225 except (TypeError, ValueError):
226 raise ncerror.SessionError(msg,
227 "No valid message-id attribute found")
228
229 try:
230 # Get the first child of rpc as the method name
231 rpc_method = rpc.getchildren()
232 if len(rpc_method) != 1:
233 log.error("badly-formatted-rpc-method", msg_id=msg_id)
234 raise ncerror.RPCSvrErrBadMsg(rpc)
235
236 rpc_method = rpc_method[0]
237
238 rpcname = rpc_method.tag.replace(qmap('nc'), "")
239 params = rpc_method.getchildren()
240
241 log.info("rpc-request", rpc=rpcname)
242
243 handler = self.main_handlers.get(rpcname, None)
244 if handler:
245 handler(self, rpcname, rpc, rpc_method, params)
246 else:
247 log.error('cannot-handle',
248 request=msg, session_id=self.session_id,
249 rpc=rpc_method)
250
251 except ncerror.RPCSvrErrBadMsg as msgerr:
252 if self.new_framing:
253 self.send_message(msgerr.get_reply_msg())
254 else:
255 # If we are 1.0 we have to simply close the connection
256 # as we are not allowed to send this error
257 log.error(
258 "Closing-1-0-session--malformed-message")
259 raise ncerror.SessionError(msg, "Malformed message")
260 except ncerror.RPCServerError as error:
261 self.send_message(error.get_reply_msg())
262 except Exception as exception:
263 error = ncerror.RPCSvrException(rpc, exception)
264 self.send_message(error.get_reply_msg())
265
266 @inlineCallbacks
267 def handle_close_session_request(self, rpcname, rpc, rpc_method,
268 params=None):
269 log.info('closing-session')
270 yield self.send_rpc_reply(etree.Element("ok"), rpc)
271 self.close()
272
273 @inlineCallbacks
274 def handle_kill_session_request(self, rpcname, rpc, rpc_method,
275 params=None):
276 log.info('killing-session')
277 yield self.send_rpc_reply(etree.Element("ok"), rpc)
278 self.close()
279
280 @inlineCallbacks
281 def handle_get_request(self, rpcname, rpc, rpc_method, params=None):
282 log.info('get')
283 if len(params) > 1:
284 raise ncerror.RPCSvrErrBadMsg(rpc)
285 if params and not utils.filter_tag_match(params[0], "nc:filter"):
286 raise ncerror.RPCSvrUnknownElement(rpc, params[0])
287 if not params:
288 params = [None]
289
290 reply = yield self.invoke_method(rpcname, rpc, params)
291 yield self.send_rpc_reply(reply, rpc)
292
293 @inlineCallbacks
294 def handle_get_config_request(self, rpcname, rpc, rpc_method, params=None):
295 log.info('get-config')
296 paramslen = len(params)
297 # Verify that the source parameter is present
298 if paramslen > 2:
299 # TODO: need to specify all elements not known
300 raise ncerror.RPCSvrErrBadMsg(rpc)
301 source_param = rpc_method.find("nc:source", namespaces=NSMAP)
302 if source_param is None:
303 raise ncerror.RPCSvrMissingElement(rpc, utils.elm("nc:source"))
304 filter_param = None
305 if paramslen == 2:
306 filter_param = rpc_method.find("nc:filter", namespaces=NSMAP)
307 if filter_param is None:
308 unknown_elm = params[0] if params[0] != source_param else \
309 params[1]
310 raise ncerror.RPCSvrUnknownElement(rpc, unknown_elm)
311 params = [source_param, filter_param]
312
313 reply = yield self.invoke_method(rpcname, rpc, params)
314 yield self.send_rpc_reply(reply, rpc)
315
316 @inlineCallbacks
317 def invoke_method(self, rpcname, rpc, params):
318 try:
319 # Handle any namespaces or prefixes in the tag, other than
320 # "nc" which was removed above. Of course, this does not handle
321 # namespace collisions, but that seems reasonable for now.
322 rpcname = rpcname.rpartition("}")[-1]
323 method_name = "rpc_" + rpcname.replace('-', '_')
324 method = getattr(self.methods, method_name,
325 self._rpc_not_implemented)
326 log.info("invoking-method", method=method_name)
327 reply = yield method(self, rpc, *params)
328 returnValue(reply)
329 except NotImplementedError:
330 raise ncerror.RPCSvrErrNotImpl(rpc)
331
332 def stop(self, reason):
333 if not self.exiting:
334 log.debug('stopping')
335 self.exiting = True
336 if self.open_session:
337 # TODO: send a closing message to the far end
338 self.conn.close_connection()
339 self.connected.callback(None)
340 self.open_session = False
341 log.info('stopped')
342
343 def close(self):
344 if not self.exiting:
345 log.debug('closing-client')
346 self.exiting = True
347 if self.open_session:
348 self.conn.close_connection()
349 self.session_open = False
350 self.connected.callback(None)
351 self.open_session = False
352 log.info('closing-client')
353
354 main_handlers = {
355 'get-config': handle_get_config_request,
356 'get': handle_get_request,
357 'kill-session': handle_kill_session_request,
358 'close-session': handle_close_session_request
359 }