blob: b5114cf98526d66ef6b33c358b79368c36058806 [file] [log] [blame]
Radek Krejcicbbb1972017-09-21 13:25:19 +02001"""
2NETCONF connections
3File: connections.py
4Author: Radek Krejci <rkrejci@cesnet.cz>
5"""
6
Radek Krejci67c922d2017-09-21 13:56:41 +02007import json
8import os
9
Radek Krejci39cce672018-06-15 16:15:18 +020010from liberouterapi import socketio, auth
Radek Krejcicbbb1972017-09-21 13:25:19 +020011from flask import request
Radek Krejci39cce672018-06-15 16:15:18 +020012from flask_socketio import emit
13from eventlet import event
14from eventlet.timeout import Timeout
Radek Krejci0f793fe2018-02-14 08:33:45 +010015import yang
Radek Krejcicbbb1972017-09-21 13:25:19 +020016import netconf2 as nc
Radek Krejcicbbb1972017-09-21 13:25:19 +020017
18from .inventory import INVENTORY
Radek Krejcia7478322018-06-19 13:09:52 +020019from .devices import devices_get, devices_replace
Radek Krejcicbbb1972017-09-21 13:25:19 +020020from .error import NetopeerException
Radek Krejcia1339602017-11-02 13:52:38 +010021from .data import *
Radek Krejcicbbb1972017-09-21 13:25:19 +020022
23sessions = {}
Radek Krejci39cce672018-06-15 16:15:18 +020024hostcheck = {}
25
26
27def hostkey_check_send(data):
28 print(json.dumps(data))
29 try:
30 e = hostcheck[data['id']]
31 e.send(data['result'])
32 except KeyError:
33 pass
34
35
36@socketio.on('hostcheck_result')
37def hostkey_check_answer(data):
38 hostkey_check_send(data)
39
Radek Krejcicbbb1972017-09-21 13:25:19 +020040
Radek Krejcie20e4d82017-11-08 14:18:05 +010041def hostkey_check(hostname, state, keytype, hexa, priv):
Radek Krejcia7478322018-06-19 13:09:52 +020042 if 'fingerprint' in priv['device']:
43 # check according to the stored fingerprint from previous connection
44 if hexa == priv['device']['fingerprint']:
45 return True
46 elif state != 2:
47 print("Incorrect host key state")
48 state = 2
49
50 # ask frontend/user for hostkey check
51 params = {'id': priv['session']['session_id'], 'hostname' : hostname, 'state' : state, 'keytype' : keytype, 'hexa' : hexa}
Radek Krejci39cce672018-06-15 16:15:18 +020052 socketio.emit('hostcheck', params, callback = hostkey_check_send)
53
54 timeout = Timeout(30)
55 try:
Radek Krejcia7478322018-06-19 13:09:52 +020056 # wait for response from the frontend
57 e = hostcheck[priv['session']['session_id']] = event.Event()
Radek Krejci39cce672018-06-15 16:15:18 +020058 result = e.wait()
59 except Timeout:
Radek Krejcia7478322018-06-19 13:09:52 +020060 # no response received within the timeout
Radek Krejci39cce672018-06-15 16:15:18 +020061 return False;
62 finally:
Radek Krejcia7478322018-06-19 13:09:52 +020063 # we have the response
64 hostcheck.pop(priv['session']['session_id'], None)
Radek Krejci39cce672018-06-15 16:15:18 +020065 timeout.cancel()
66
Radek Krejcia7478322018-06-19 13:09:52 +020067 if result:
68 # store confirmed fingerprint for future connections
69 priv['device']['fingerprint'] = hexa;
70 devices_replace(priv['device']['id'], priv['session']['user'].username, priv['device'])
71
Radek Krejci39cce672018-06-15 16:15:18 +020072 return result
73
Radek Krejcie20e4d82017-11-08 14:18:05 +010074
Radek Krejcicbbb1972017-09-21 13:25:19 +020075@auth.required()
76def connect():
77 session = auth.lookup(request.headers.get('Authorization', None))
78 user = session['user']
79 path = os.path.join(INVENTORY, user.username)
80
Radek Krejcia6c8b412017-10-17 16:59:38 +020081 data = request.get_json()
82 if 'id' in data:
83 # stored device
84 device = devices_get(data['id'], user.username)
85 elif 'device' in data:
86 # one-time connect, the device is specified in request
87 device = data['device']
88 else:
89 raise NetopeerException('Invalid connection request.')
90
Radek Krejcicbbb1972017-09-21 13:25:19 +020091 if not device:
92 raise NetopeerException('Unknown device to connect to request.')
93
94 nc.setSearchpath(path)
95
96 ssh = nc.SSH(device['username'], password=device['password'])
Radek Krejcia7478322018-06-19 13:09:52 +020097 ssh.setAuthHostkeyCheckClb(hostkey_check, {'session': session, 'device' : device})
Radek Krejcicbbb1972017-09-21 13:25:19 +020098 try:
Radek Krejci39cce672018-06-15 16:15:18 +020099 ncs = nc.Session(device['hostname'], device['port'], ssh)
Radek Krejcicbbb1972017-09-21 13:25:19 +0200100 except Exception as e:
101 return(json.dumps({'success': False, 'error-msg': str(e)}))
102
103 if not user.username in sessions:
104 sessions[user.username] = {}
105
106 # use key (as hostname:port:session-id) to store the created NETCONF session
Radek Krejci39cce672018-06-15 16:15:18 +0200107 key = ncs.host + ":" + str(ncs.port) + ":" + ncs.id
Radek Krejci4d3896c2018-01-08 17:10:43 +0100108 sessions[user.username][key] = {}
Radek Krejci39cce672018-06-15 16:15:18 +0200109 sessions[user.username][key]['session'] = ncs
Radek Krejcicbbb1972017-09-21 13:25:19 +0200110
111 return(json.dumps({'success': True, 'session-key': key}))
112
113
114@auth.required()
115def session_get_capabilities():
116 session = auth.lookup(request.headers.get('Authorization', None))
117 user = session['user']
118 req = request.args.to_dict()
119
120 if not 'key' in req:
121 return(json.dumps({'success': False, 'error-msg': 'Missing session key.'}))
122
123 if not user.username in sessions:
124 sessions[user.username] = {}
125
126 key = req['key']
127 if not key in sessions[user.username]:
128 return(json.dumps({'success': False, 'error-msg': 'Invalid session key.'}))
129
130 cpblts = []
Radek Krejci4d3896c2018-01-08 17:10:43 +0100131 for c in sessions[user.username][key]['session'].capabilities:
Radek Krejcicbbb1972017-09-21 13:25:19 +0200132 cpblts.append(c)
133
134 return(json.dumps({'success': True, 'capabilities': cpblts}))
135
136@auth.required()
Radek Krejci2e578562017-10-17 11:11:13 +0200137def session_get():
138 session = auth.lookup(request.headers.get('Authorization', None))
139 user = session['user']
140 req = request.args.to_dict()
141
142 if not 'key' in req:
143 return(json.dumps({'success': False, 'error-msg': 'Missing session key.'}))
Radek Krejcia1339602017-11-02 13:52:38 +0100144 if not 'recursive' in req:
145 return(json.dumps({'success': False, 'error-msg': 'Missing recursive flag.'}))
Radek Krejci2e578562017-10-17 11:11:13 +0200146
147 if not user.username in sessions:
148 sessions[user.username] = {}
149
150 key = req['key']
151 if not key in sessions[user.username]:
152 return(json.dumps({'success': False, 'error-msg': 'Invalid session key.'}))
153
Radek Krejci2e578562017-10-17 11:11:13 +0200154 try:
Radek Krejci4d3896c2018-01-08 17:10:43 +0100155 sessions[user.username][key]['data'] = sessions[user.username][key]['session'].rpcGet()
Radek Krejci010475d2018-03-08 13:14:19 +0100156 except ConnectionError as e:
157 reply = {'success': False, 'error': [{'msg': str(e)}]}
158 del sessions[user.username][key]
159 return(json.dumps(reply))
Radek Krejci2e578562017-10-17 11:11:13 +0200160 except nc.ReplyError as e:
161 reply = {'success': False, 'error': []}
162 for err in e.args[0]:
163 reply['error'].append(json.loads(str(err)))
164 return(json.dumps(reply))
165
Radek Krejcia1339602017-11-02 13:52:38 +0100166 if not 'path' in req:
Radek Krejci4d3896c2018-01-08 17:10:43 +0100167 return(dataInfoRoots(sessions[user.username][key]['data'], True if req['recursive'] == 'true' else False))
Radek Krejcia1339602017-11-02 13:52:38 +0100168 else:
Radek Krejci4d3896c2018-01-08 17:10:43 +0100169 return(dataInfoSubtree(sessions[user.username][key]['data'], req['path'], True if req['recursive'] == 'true' else False))
170
171
Radek Krejci6e772b22018-01-25 13:28:57 +0100172def _checkvalue(session, req, schema):
173 user = session['user'];
Radek Krejci4d3896c2018-01-08 17:10:43 +0100174
175 if not 'key' in req:
176 return(json.dumps({'success': False, 'error-msg': 'Missing session key.'}))
177 if not 'path' in req:
178 return(json.dumps({'success': False, 'error-msg': 'Missing path to validate value.'}))
179 if not 'value' in req:
180 return(json.dumps({'success': False, 'error-msg': 'Missing value to validate.'}))
181
Radek Krejci6e772b22018-01-25 13:28:57 +0100182 key = req['key']
183 if not key in sessions[user.username]:
184 return(json.dumps({'success': False, 'error-msg': 'Invalid session key.'}))
185
Radek Krejcicf719cb2018-02-15 16:47:32 +0100186 ctx = sessions[user.username][key]['session'].context;
Radek Krejci6e772b22018-01-25 13:28:57 +0100187 if schema:
Radek Krejcicf719cb2018-02-15 16:47:32 +0100188 search = ctx.find_path(req['path'])
Radek Krejci6e772b22018-01-25 13:28:57 +0100189 else:
190 search = sessions[user.username][key]['data'].find_path(req['path'])
191
192 if search.number() != 1:
193 return(json.dumps({'success': False, 'error-msg': 'Invalid data path.'}))
194
195 if schema:
196 node = search.schema()[0]
197 else:
198 node = search.data()[0]
199
200 if node.validate_value(req['value']):
Radek Krejcicf719cb2018-02-15 16:47:32 +0100201 errors = yang.get_ly_errors(ctx)
202 if errors.size():
203 return(json.dumps({'success': False, 'error-msg': errors[errors.size() - 1].errmsg()}))
204 else:
205 return(json.dumps({'success': False, 'error-msg': 'unknown error'}))
Radek Krejci6e772b22018-01-25 13:28:57 +0100206
207 return(json.dumps({'success': True}))
208
209@auth.required()
210def data_checkvalue():
211 session = auth.lookup(request.headers.get('Authorization', None))
212 req = request.args.to_dict()
213
214 return _checkvalue(session, req, False)
215
216
217@auth.required()
218def schema_checkvalue():
219 session = auth.lookup(request.headers.get('Authorization', None))
220 req = request.args.to_dict()
221
222 return _checkvalue(session, req, True)
223
224
225@auth.required()
Radek Krejcid0ce4cf2018-02-09 14:44:34 +0100226def schema_values():
227 session = auth.lookup(request.headers.get('Authorization', None))
228 user = session['user']
229 req = request.args.to_dict()
230
231 if not 'key' in req:
232 return(json.dumps({'success': False, 'error-msg': 'Missing session key.'}))
233 if not 'path' in req:
234 return(json.dumps({'success': False, 'error-msg': 'Missing path to validate value.'}))
235
236 key = req['key']
237 if not key in sessions[user.username]:
238 return(json.dumps({'success': False, 'error-msg': 'Invalid session key.'}))
239
240 search = sessions[user.username][key]['session'].context.find_path(req['path'])
241 if search.number() != 1:
242 return(json.dumps({'success': False, 'error-msg': 'Invalid data path.'}))
243 schema = search.schema()[0]
244
Radek Krejci0f793fe2018-02-14 08:33:45 +0100245 if schema.nodetype() != yang.LYS_LEAF and schema.nodetype != yang.LYS_LEAFLIST:
Radek Krejcid0ce4cf2018-02-09 14:44:34 +0100246 result = None
247 else:
248 result = typeValues(schema.subtype().type(), [])
249 return(json.dumps({'success': True, 'data': result}))
250
251
252@auth.required()
Radek Krejci6e772b22018-01-25 13:28:57 +0100253def schema_info():
254 session = auth.lookup(request.headers.get('Authorization', None))
255 user = session['user']
256 req = request.args.to_dict()
257
258 if not 'key' in req:
259 return(json.dumps({'success': False, 'error-msg': 'Missing session key.'}))
260 if not 'path' in req:
261 return(json.dumps({'success': False, 'error-msg': 'Missing path to validate value.'}))
Radek Krejci4d3896c2018-01-08 17:10:43 +0100262
263 key = req['key']
264 if not key in sessions[user.username]:
265 return(json.dumps({'success': False, 'error-msg': 'Invalid session key.'}))
266
Radek Krejci6e772b22018-01-25 13:28:57 +0100267 if req['path'] == '/':
Radek Krejcif71867f2018-01-30 13:28:28 +0100268 node = None
Radek Krejci6e772b22018-01-25 13:28:57 +0100269 else:
270 search = sessions[user.username][key]['session'].context.find_path(req['path'])
271 if search.number() != 1:
272 return(json.dumps({'success': False, 'error-msg': 'Invalid data path.'}))
273 node = search.schema()[0]
Radek Krejci4d3896c2018-01-08 17:10:43 +0100274
Radek Krejci6e772b22018-01-25 13:28:57 +0100275 result = [];
276 if 'relative' in req:
277 if req['relative'] == 'children':
Radek Krejcif71867f2018-01-30 13:28:28 +0100278 if node:
279 instantiables = node.child_instantiables(0)
280 else:
281 # top level
282 instantiables = sessions[user.username][key]['session'].context.data_instantiables(0)
Radek Krejci6e772b22018-01-25 13:28:57 +0100283 elif req['relative'] == 'siblings':
284 if node.parent():
Radek Krejcif71867f2018-01-30 13:28:28 +0100285 instantiables = node.parent().child_instantiables(0)
286 else:
287 # top level
288 instantiables = sessions[user.username][key]['session'].context.data_instantiables(0)
Radek Krejci6e772b22018-01-25 13:28:57 +0100289 else:
290 return(json.dumps({'success': False, 'error-msg': 'Invalid relative parameter.'}))
Radek Krejcif71867f2018-01-30 13:28:28 +0100291
292 for child in instantiables:
Radek Krejci0f793fe2018-02-14 08:33:45 +0100293 if child.flags() & yang.LYS_CONFIG_R:
Radek Krejcif71867f2018-01-30 13:28:28 +0100294 # ignore status nodes
295 continue
Radek Krejci0f793fe2018-02-14 08:33:45 +0100296 if child.nodetype() & (yang.LYS_RPC | yang.LYS_NOTIF | yang.LYS_ACTION):
Radek Krejcif71867f2018-01-30 13:28:28 +0100297 # ignore RPCs, Notifications and Actions
298 continue
299 result.append(schemaInfoNode(child))
Radek Krejci6e772b22018-01-25 13:28:57 +0100300 else:
301 result.append(schemaInfoNode(node))
Radek Krejci4d3896c2018-01-08 17:10:43 +0100302
Radek Krejci6e772b22018-01-25 13:28:57 +0100303 return(json.dumps({'success': True, 'data': result}))
Radek Krejci2e578562017-10-17 11:11:13 +0200304
305
Radek Krejcif041d652018-02-06 10:21:25 +0100306def _create_child(ctx, parent, child_def):
Radek Krejciad45e572018-02-21 10:54:54 +0100307 at = child_def['info']['module'].find('@')
308 if at == -1:
309 module = ctx.get_module(child_def['info']['module'])
310 else:
311 module = ctx.get_module(child_def['info']['module'][:at], child_def['info']['module'][at + 1:])
Radek Krejcif041d652018-02-06 10:21:25 +0100312 # print('child: ' + json.dumps(child_def))
Radek Krejci7a244f02018-02-21 09:46:18 +0100313 # print('parent: ' + parent.schema().name())
314 # print('module: ' + module.name())
315 # print('name: ' + child_def['info']['name'])
316 if child_def['info']['type'] == 4 :
317 # print('value: ' + str(child_def['value']))
Radek Krejci0f793fe2018-02-14 08:33:45 +0100318 yang.Data_Node(parent, module, child_def['info']['name'], child_def['value'])
Radek Krejci7a244f02018-02-21 09:46:18 +0100319 elif child_def['info']['type'] == 8:
320 # print('value: ' + child_def['value'][0])
321 yang.Data_Node(parent, module, child_def['info']['name'], child_def['value'][0])
Radek Krejcif041d652018-02-06 10:21:25 +0100322 else:
Radek Krejci0f793fe2018-02-14 08:33:45 +0100323 child = yang.Data_Node(parent, module, child_def['info']['name'])
Radek Krejcif041d652018-02-06 10:21:25 +0100324 if 'children' in child_def:
325 for grandchild in child_def['children']:
326 _create_child(ctx, child, grandchild)
327
328
329@auth.required()
330def session_commit():
331 session = auth.lookup(request.headers.get('Authorization', None))
332 user = session['user']
333
Radek Krejci5a93f9d2018-03-21 13:17:18 +0100334 req = request.get_json(keep_order = True)
Radek Krejcif041d652018-02-06 10:21:25 +0100335 if not 'key' in req:
336 return(json.dumps({'success': False, 'error-msg': 'Missing session key.'}))
337 if not 'modifications' in req:
338 return(json.dumps({'success': False, 'error-msg': 'Missing modifications.'}))
339
340 mods = req['modifications']
341 ctx = sessions[user.username][req['key']]['session'].context
342 root = None
Radek Krejci010475d2018-03-08 13:14:19 +0100343 reorders = []
Radek Krejcif041d652018-02-06 10:21:25 +0100344 for key in mods:
345 recursion = False
346 # get correct path and value if needed
347 path = key
348 value = None
349 if mods[key]['type'] == 'change':
350 value = mods[key]['value']
351 elif mods[key]['type'] == 'create' or mods[key]['type'] == 'replace':
Radek Krejci7a244f02018-02-21 09:46:18 +0100352 if mods[key]['data']['info']['type'] == 1:
353 # creating/replacing container
354 recursion = True
355 elif mods[key]['data']['info']['type'] == 4:
Radek Krejcif041d652018-02-06 10:21:25 +0100356 # creating/replacing leaf
357 value = mods[key]['data']['value']
Radek Krejci7a244f02018-02-21 09:46:18 +0100358 elif mods[key]['data']['info']['type'] == 8:
359 # creating/replacing leaf-list
360 value = mods[key]['data']['value'][0]
361 path = mods[key]['data']['path']
Radek Krejcif041d652018-02-06 10:21:25 +0100362 elif mods[key]['data']['info']['type'] == 16:
363 recursion = True
364 path = mods[key]['data']['path']
Radek Krejci010475d2018-03-08 13:14:19 +0100365 elif mods[key]['type'] == 'reorder':
366 # postpone reorders
367 reorders.extend(mods[key]['transactions'])
368 continue
Radek Krejcif041d652018-02-06 10:21:25 +0100369
370 # create node
Radek Krejci7a244f02018-02-21 09:46:18 +0100371 # print("creating " + path)
372 # print("value " + str(value))
Radek Krejcif041d652018-02-06 10:21:25 +0100373 if root:
374 root.new_path(ctx, path, value, 0, 0)
375 else:
Radek Krejci0f793fe2018-02-14 08:33:45 +0100376 root = yang.Data_Node(ctx, path, value, 0, 0)
Radek Krejcif041d652018-02-06 10:21:25 +0100377 node = root.find_path(path).data()[0];
378
379 # set operation attribute and add additional data if any
380 if mods[key]['type'] == 'change':
381 node.insert_attr(None, 'ietf-netconf:operation', 'merge')
382 elif mods[key]['type'] == 'delete':
383 node.insert_attr(None, 'ietf-netconf:operation', 'delete')
384 elif mods[key]['type'] == 'create':
385 node.insert_attr(None, 'ietf-netconf:operation', 'create')
386 elif mods[key]['type'] == 'replace':
387 node.insert_attr(None, 'ietf-netconf:operation', 'replace')
388 else:
389 return(json.dumps({'success': False, 'error-msg': 'Invalid modification ' + key}))
390
391 if recursion and 'children' in mods[key]['data']:
392 for child in mods[key]['data']['children']:
Radek Krejcif73403b2018-02-08 14:57:25 +0100393 if 'key' in child['info'] and child['info']['key']:
Radek Krejcif041d652018-02-06 10:21:25 +0100394 continue
395 _create_child(ctx, node, child)
396
Radek Krejci010475d2018-03-08 13:14:19 +0100397 # finally process reorders which must be last since they may refer newly created nodes
398 # and they do not reflect removed nodes
399 for move in reorders:
400 try:
401 node = root.find_path(move['node']).data()[0];
402 parent = node.parent()
403 node.unlink()
404 if parent:
405 parent.insert(node)
406 else:
407 root.insert_sibling(node)
408 except:
409 if root:
410 root.new_path(ctx, move['node'], None, 0, 0)
411 else:
412 root = yang.Data_Node(ctx, move['node'], None, 0, 0)
413 node = root.find_path(move['node']).data()[0];
414 node.insert_attr(None, 'yang:insert', move['insert'])
415 if move['insert'] == 'after' or move['insert'] == 'before':
416 if 'key' in move:
417 node.insert_attr(None, 'yang:key', move['key'])
418 elif 'value' in move:
419 node.insert_attr(None, 'yang:value', move['value'])
420
Radek Krejci0f793fe2018-02-14 08:33:45 +0100421 # print(root.print_mem(yang.LYD_XML, yang.LYP_FORMAT))
Radek Krejcif73403b2018-02-08 14:57:25 +0100422 try:
423 sessions[user.username][req['key']]['session'].rpcEditConfig(nc.DATASTORE_RUNNING, root)
424 except nc.ReplyError as e:
425 reply = {'success': False, 'error': []}
426 for err in e.args[0]:
427 reply['error'].append(json.loads(str(err)))
428 return(json.dumps(reply))
429
Radek Krejcif041d652018-02-06 10:21:25 +0100430 return(json.dumps({'success': True}))
431
432
Radek Krejci2e578562017-10-17 11:11:13 +0200433@auth.required()
Radek Krejcicbbb1972017-09-21 13:25:19 +0200434def session_close():
435 session = auth.lookup(request.headers.get('Authorization', None))
436 user = session['user']
437 req = request.args.to_dict()
438
439 if not 'key' in req:
440 return(json.dumps({'success': False, 'error-msg': 'Missing session key.'}))
441
442 if not user.username in sessions:
443 sessions[user.username] = {}
444
445 key = req['key']
446 if not key in sessions[user.username]:
447 return(json.dumps({'success': False, 'error-msg': 'Invalid session key.'}))
448
449 del sessions[user.username][key]
450 return(json.dumps({'success': True}))
451
452@auth.required()
453def session_alive():
454 session = auth.lookup(request.headers.get('Authorization', None))
455 user = session['user']
456 req = request.args.to_dict()
457
458 if not 'key' in req:
459 return(json.dumps({'success': False, 'error-msg': 'Missing session key.'}))
460
461 if not user.username in sessions:
462 sessions[user.username] = {}
463
464 key = req['key']
465 if not key in sessions[user.username]:
466 return(json.dumps({'success': False, 'error-msg': 'Invalid session key.'}))
467
468 return(json.dumps({'success': True}))