blob: 907aff008700481871e6d2728f0adb860c82d5ca [file] [log] [blame]
Jakub Man81d1f0c2020-12-21 10:04:09 +01001# coding=utf-8
2"""
3Netconf session operations
4File: netconf.py
5Author: Jakub Man <xmanja00@stud.fit.vutbr.cz>
6
7
8Parts of this file was taken from the Netopeer2GUI project by Radek Krejčí
9Available here: https://github.com/CESNET/Netopeer2GUI
10
11 Copyright 2017 Radek Krejčí
12
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
16
17 http://www.apache.org/licenses/LICENSE-2.0
18
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
24"""
25
26from liberouterapi import db, auth, config, socketio
27from liberouterapi.dbConnector import dbConnector
28import netconf2 as nc
29import json
30from eventlet.timeout import Timeout
31from flask import request
32import logging
33from .sockets import *
34import os
35import yang
36from .schemas import get_schema
37from .devices import *
38from .data import *
39import pprint
40
41sessions = {}
42log = logging.getLogger(__name__)
43netconf_db = dbConnector('netconf', provider='mongodb', config={'database': config['netconf']['database']})
44netconf_coll = netconf_db.db[config['netconf']['collection']]
45
46"""
47netconf session (ncs)
48static PyGetSetDef ncSessionGetSetters[] = {
49 {"id", (getter)ncSessionGetId, NULL, "NETCONF Session id.", NULL},
50 {"host", (getter)ncSessionGetHost, NULL, "Host where the NETCONF Session is connected.", NULL},
51 {"port", (getter)ncSessionGetPort, NULL, "Port number where the NETCONF Session is connected.", NULL},
52 {"user", (getter)ncSessionGetUser, NULL, "Username of the user connected with the NETCONF Session.", NULL},
53 {"transport", (getter)ncSessionGetTransport, NULL, "Transport protocol used for the NETCONF Session.", NULL},
54 {"version", (getter)ncSessionGetVersion, NULL, "NETCONF Protocol version used for the NETCONF Session.", NULL},
55 {"capabilities", (getter)ncSessionGetCapabilities, NULL, "Capabilities of the NETCONF Session.", NULL},
56 {"context", (getter)ncSessionGetContext, NULL, "libyang context of the NETCONF Session.", NULL},
57 {NULL} /* Sentinel */
58};
59"""
60
61
62def auth_common(session_id):
63 global log
64 result = None
65 timeout = Timeout(60)
66 try:
67 # wait for response from the frontend
68 data = sio_wait(session_id)
69 result = data['password']
70 except Timeout:
71 # no response received within the timeout
72 log.info("socketio: auth request timeout.")
73 except KeyError:
74 # no password
75 log.info("socketio: invalid credential data received.")
76 finally:
77 # we have the response
78 sio_clean(session_id)
79 timeout.cancel()
80
81 return result
82
83
84def auth_interactive(name, instruction, prompt, priv):
85 try:
86 pp = pprint.PrettyPrinter(indent=4)
87 pp.pprint(priv)
88 params = {'id': priv['id'], 'type': name, 'msg': instruction, 'prompt': prompt, 'device': priv['device']}
89 sio_emit('device_auth', params)
90 return auth_common(priv)
91 except Exception as e:
92 print(e)
93
94
95def auth_password(username, hostname, priv):
96 sio_emit('device_auth', {'id': priv, 'type': 'Password Authentication', 'msg': username + '@' + hostname})
97 return auth_common(priv)
98
99
100@auth.required()
101def connect_device():
102 global sessions
103 session = auth.lookup(request.headers.get('lgui-Authorization', None))
104 username = str(session['user'].username)
105 data = request.get_json()
106
107 nc.setSchemaCallback(get_schema, session)
108 site_root = os.path.realpath(os.path.dirname(__file__))
109 path = os.path.join(site_root, 'userfiles', username)
110 if not os.path.exists(path):
111 os.makedirs(path)
112 nc.setSearchpath(path)
Jakub Mane5363e82021-02-04 12:35:32 +0100113 if 'password' in data and data['password'] != '' and data['password'] is not None:
Jakub Man81d1f0c2020-12-21 10:04:09 +0100114 ssh = nc.SSH(data['username'], password=data['password'])
115 else:
116 ssh = nc.SSH(data['username'])
117 ssh.setAuthPasswordClb(auth_password, session['session_id'])
118 ssh.setAuthInteractiveClb(func=auth_interactive, priv={'id': session['session_id'], 'device': data})
119
120 ssh.setAuthHostkeyCheckClb(hostkey_check, {'session': session, 'device': data})
121
122 try:
123 ncs = nc.Session(data['hostname'], int(data['port']), ssh)
124 except Exception as e:
125 nc.setSchemaCallback(None)
126 return json.dumps({'success': False, 'code': 500, 'message': str(e)})
127 nc.setSchemaCallback(None)
128
129 if username not in sessions:
130 sessions[username] = {}
131
132 # use key (as hostname:port:session-id) to store the created NETCONF session
133 key = ncs.host + ":" + str(ncs.port) + ":" + ncs.id
134 sessions[username][key] = {}
135 sessions[username][key]['session'] = ncs
136
137 # update inventory's list of schemas
138 # schemas_update(session)
139
140 return json.dumps({'success': True, 'session-key': key})
141
142
143def hostkey_check(hostname, state, keytype, hexa, priv):
144 if 'fingerprint' in priv['device']:
145 # check according to the stored fingerprint from previous connection
146 if hexa == priv['device']['fingerprint']:
147 return True
148 elif state != 2:
149 log.error("Incorrect host key state")
150 state = 2
151
152 # ask frontend/user for hostkey check
153 params = {'id': priv['session']['session_id'], 'hostname': hostname, 'state': state, 'keytype': keytype,
154 'hexa': hexa, 'device': priv['device']}
155 sio_emit('hostcheck', params)
156
157 result = False
158 timeout = Timeout(30)
159 try:
160 # wait for response from the frontend
161 data = sio_wait(priv['session']['session_id'])
162 result = data['result']
163 except Timeout:
164 # no response received within the timeout
165 log.info("socketio: hostcheck timeout.")
166 except KeyError:
167 # invalid response
168 log.error("socketio: invalid hostcheck_result received.")
169 finally:
170 # we have the response
171 sio_clean(priv['session']['session_id'])
172 timeout.cancel()
173
174 if result:
175 # store confirmed fingerprint for future connections
176 priv['device']['fingerprint'] = hexa
177 if 'id' in priv['device'].keys():
178 update_hexa(priv['device']['id'], hexa, netconf_coll)
179 else:
180 update_hexa_by_device(priv['device'], hexa, netconf_coll)
181 return result
182
183
184""" SESSION HANDLING """
185
186
187@auth.required()
188def sessions_get_open():
189 """
190 Get all open sessions belonging to the user
191 :return: Array of session keys and devices belonging to sessions. Device names will not be loaded.
192 """
193 global sessions
194 session = auth.lookup(request.headers.get('lgui-Authorization', None))
195 username = str(session['user'].username)
196
197 if username in sessions:
198 result = []
199 for key in sessions[username].keys():
200 ncs = sessions[username][key]['session']
201 device = get_device_from_session_data(ncs.host, ncs.port, username, ncs.user, netconf_coll)
202 if device is not None:
203 result.append({'key': key, 'device': device})
204 return json.dumps(result)
205 else:
206 return json.dumps([])
207
208
209@auth.required()
210def session_alive(key):
211 global sessions
212 session = auth.lookup(request.headers.get('lgui-Authorization', None))
213 username = str(session['user'].username)
214
215 if not username in sessions:
216 sessions[username] = {}
217
218 if key in sessions[username]:
219 return json.dumps({'success': True, 'code': 200})
220 else:
221 return json.dumps({'success': False, 'code': 404, 'message': 'Session not found'})
222
223
224@auth.required()
225def session_destroy(key):
226 global sessions
227 session = auth.lookup(request.headers.get('lgui-Authorization', None))
228 username = str(session['user'].username)
229 if not username in sessions:
230 sessions[username] = {}
231
232 if key in sessions[username]:
233 del sessions[username][key]
234 return json.dumps({'success': True, 'code': 200})
235 else:
236 return json.dumps({'success': False, 'code': 404, 'message': 'Session not found'})
237
238
239@auth.required()
240def session_destroy_all():
241 global sessions
242 session = auth.lookup(request.headers.get('lgui-Authorization', None))
243 username = str(session['user'].username)
244 if username in sessions:
245 del sessions[username]
246 return json.dumps({'success': True, 'code': 200})
247
248
249@auth.required()
250def session_rpc_get():
251 """
252 code 500: wrong argument
253 code 404: session invalid -> try reconnecting
254 code 410: connection gone -> remove session, try reconnecting
255 code 418: Error in processing netconf request (nothing to do with a teapot)
256 """
257 global sessions
258 session = auth.lookup(request.headers.get('lgui-Authorization', None))
259 username = str(session['user'].username)
260 req = request.args.to_dict()
261 if 'key' not in req:
262 return json.dumps({'success': False, 'code': 500, 'message': 'Missing session key.'})
263 if 'recursive' not in req:
264 return json.dumps({'success': False, 'code': 500, 'message': 'Missing recursive flag.'})
265
266 if username not in sessions:
267 sessions[username] = {}
268
269 key = req['key']
270 if key not in sessions[username]:
271 return json.dumps({'success': False, 'code': 404, 'message': 'Invalid session key.'})
272
273 try:
274 sessions[username][key]['data'] = sessions[username][key]['session'].rpcGet()
275 except ConnectionError as e:
276 del sessions[username][key]
277 return json.dumps({'success': False, 'code': 410, 'message': str(e)})
278 except nc.ReplyError as e:
279 err_list = []
280 for err in e.args[0]:
281 err_list.append(str(err))
282 return json.dumps({'success': False, 'code': 418, 'message': str(e)})
283
284 if 'path' not in req:
285 return data_info_roots(sessions[username][key]['data'], True if req['recursive'] == 'true' else False)
286 else:
287 return data_info_subtree(sessions[username][key]['data'], req['path'],
288 True if req['recursive'] == 'true' else False)
289
290
291@auth.required()
292def session_commit():
293 session = auth.lookup(request.headers.get('lgui-Authorization', None))
294 user = session['user']
295
296 req = request.get_json(keep_order=True)
297 if 'key' not in req:
298 return json.dumps({'success': False, 'code': 500, 'message': 'Missing session key.'})
299 if 'modifications' not in req:
300 return json.dumps({'success': False, 'code': 500, 'message': 'Missing modifications.'})
301
302 mods = req['modifications']
303 ctx = sessions[user.username][req['key']]['session'].context
304 root = None
305 reorders = []
306 for key in mods:
307 recursion = False
308 # get correct path and value if needed
309 path = mods[key]['data']['path']
310 value = None
311 if mods[key]['type'] == 'change':
312 value = mods[key]['value']
313 elif mods[key]['type'] == 'create' or mods[key]['type'] == 'replace':
314 if mods[key]['data']['info']['type'] == 1:
315 # creating/replacing container
316 recursion = True
317 elif mods[key]['data']['info']['type'] == 4:
318 # creating/replacing leaf
319 value = mods[key]['data']['value']
320 elif mods[key]['data']['info']['type'] == 8:
321 # creating/replacing leaf-list
322 value = mods[key]['data']['value'][0]
323 path = mods[key]['data']['path']
324 elif mods[key]['data']['info']['type'] == 16:
325 recursion = True
326 path = mods[key]['data']['path']
327 elif mods[key]['type'] == 'reorder':
328 # postpone reorders
329 reorders.extend(mods[key]['transactions'])
330 continue
331
332 # create node
333 # print("creating " + path)
334 # print("value " + str(value))
335 if root:
336 root.new_path(ctx, path, value, 0, 0)
337 else:
338 try:
339 root = yang.Data_Node(ctx, path, value, 0, 0)
340 except Exception as e:
341 print(e)
342 return json.dumps({'success': False, 'code': 404, 'message': str(e)})
343 node = root.find_path(path).data()[0]
344
345 # set operation attribute and add additional data if any
346 if mods[key]['type'] == 'change':
347 node.insert_attr(None, 'ietf-netconf:operation', 'merge')
348 elif mods[key]['type'] == 'delete':
349 node.insert_attr(None, 'ietf-netconf:operation', 'delete')
350 elif mods[key]['type'] == 'create':
351 node.insert_attr(None, 'ietf-netconf:operation', 'create')
352 elif mods[key]['type'] == 'replace':
353 node.insert_attr(None, 'ietf-netconf:operation', 'replace')
354 else:
355 return json.dumps({'success': False, 'error-msg': 'Invalid modification ' + key})
356
357 if recursion and 'children' in mods[key]['data']:
358 for child in mods[key]['data']['children']:
359 if 'key' in child['info'] and child['info']['key']:
360 continue
361 _create_child(ctx, node, child)
362
363 # finally process reorders which must be last since they may refer newly created nodes
364 # and they do not reflect removed nodes
365 for move in reorders:
366 try:
367 node = root.find_path(move['node']).data()[0];
368 parent = node.parent()
369 node.unlink()
370 if parent:
371 parent.insert(node)
372 else:
373 root.insert_sibling(node)
374 except Exception as e:
375 if root:
376 root.new_path(ctx, move['node'], None, 0, 0)
377 else:
378 root = yang.Data_Node(ctx, move['node'], None, 0, 0)
379 node = root.find_path(move['node']).data()[0];
380 node.insert_attr(None, 'yang:insert', move['insert'])
381 if move['insert'] == 'after' or move['insert'] == 'before':
382 if 'key' in move:
383 node.insert_attr(None, 'yang:key', move['key'])
384 elif 'value' in move:
385 node.insert_attr(None, 'yang:value', move['value'])
386
387 # print(root.print_mem(yang.LYD_XML, yang.LYP_FORMAT))
388 try:
389 sessions[user.username][req['key']]['session'].rpcEditConfig(nc.DATASTORE_RUNNING, root)
390 except nc.ReplyError as e:
391 reply = {'success': False, 'code': 500, 'message': '[]'}
392 for err in e.args[0]:
393 reply['message'] += str(err) + '; '
394 return json.dumps(reply)
395
396 return json.dumps({'success': True})
397
398
399def _create_child(ctx, parent, child_def):
400 at = child_def['info']['module'].find('@')
401 if at == -1:
402 module = ctx.get_module(child_def['info']['module'])
403 else:
404 module = ctx.get_module(child_def['info']['module'][:at], child_def['info']['module'][at + 1:])
405 if child_def['info']['type'] == 4:
406 yang.Data_Node(parent, module, child_def['info']['name'], child_def['value'])
407 elif child_def['info']['type'] == 8:
408 yang.Data_Node(parent, module, child_def['info']['name'], child_def['value'][0])
409 else:
410 child = yang.Data_Node(parent, module, child_def['info']['name'])
411 if 'children' in child_def:
412 for grandchild in child_def['children']:
413 _create_child(ctx, child, grandchild)