blob: 907aff008700481871e6d2728f0adb860c82d5ca [file] [log] [blame]
# coding=utf-8
"""
Netconf session operations
File: netconf.py
Author: Jakub Man <xmanja00@stud.fit.vutbr.cz>
Parts of this file was taken from the Netopeer2GUI project by Radek Krejčí
Available here: https://github.com/CESNET/Netopeer2GUI
Copyright 2017 Radek Krejčí
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from liberouterapi import db, auth, config, socketio
from liberouterapi.dbConnector import dbConnector
import netconf2 as nc
import json
from eventlet.timeout import Timeout
from flask import request
import logging
from .sockets import *
import os
import yang
from .schemas import get_schema
from .devices import *
from .data import *
import pprint
sessions = {}
log = logging.getLogger(__name__)
netconf_db = dbConnector('netconf', provider='mongodb', config={'database': config['netconf']['database']})
netconf_coll = netconf_db.db[config['netconf']['collection']]
"""
netconf session (ncs)
static PyGetSetDef ncSessionGetSetters[] = {
{"id", (getter)ncSessionGetId, NULL, "NETCONF Session id.", NULL},
{"host", (getter)ncSessionGetHost, NULL, "Host where the NETCONF Session is connected.", NULL},
{"port", (getter)ncSessionGetPort, NULL, "Port number where the NETCONF Session is connected.", NULL},
{"user", (getter)ncSessionGetUser, NULL, "Username of the user connected with the NETCONF Session.", NULL},
{"transport", (getter)ncSessionGetTransport, NULL, "Transport protocol used for the NETCONF Session.", NULL},
{"version", (getter)ncSessionGetVersion, NULL, "NETCONF Protocol version used for the NETCONF Session.", NULL},
{"capabilities", (getter)ncSessionGetCapabilities, NULL, "Capabilities of the NETCONF Session.", NULL},
{"context", (getter)ncSessionGetContext, NULL, "libyang context of the NETCONF Session.", NULL},
{NULL} /* Sentinel */
};
"""
def auth_common(session_id):
global log
result = None
timeout = Timeout(60)
try:
# wait for response from the frontend
data = sio_wait(session_id)
result = data['password']
except Timeout:
# no response received within the timeout
log.info("socketio: auth request timeout.")
except KeyError:
# no password
log.info("socketio: invalid credential data received.")
finally:
# we have the response
sio_clean(session_id)
timeout.cancel()
return result
def auth_interactive(name, instruction, prompt, priv):
try:
pp = pprint.PrettyPrinter(indent=4)
pp.pprint(priv)
params = {'id': priv['id'], 'type': name, 'msg': instruction, 'prompt': prompt, 'device': priv['device']}
sio_emit('device_auth', params)
return auth_common(priv)
except Exception as e:
print(e)
def auth_password(username, hostname, priv):
sio_emit('device_auth', {'id': priv, 'type': 'Password Authentication', 'msg': username + '@' + hostname})
return auth_common(priv)
@auth.required()
def connect_device():
global sessions
session = auth.lookup(request.headers.get('lgui-Authorization', None))
username = str(session['user'].username)
data = request.get_json()
nc.setSchemaCallback(get_schema, session)
site_root = os.path.realpath(os.path.dirname(__file__))
path = os.path.join(site_root, 'userfiles', username)
if not os.path.exists(path):
os.makedirs(path)
nc.setSearchpath(path)
if 'password' in data and data['password'] != '' and data['password'] is not None:
ssh = nc.SSH(data['username'], password=data['password'])
else:
ssh = nc.SSH(data['username'])
ssh.setAuthPasswordClb(auth_password, session['session_id'])
ssh.setAuthInteractiveClb(func=auth_interactive, priv={'id': session['session_id'], 'device': data})
ssh.setAuthHostkeyCheckClb(hostkey_check, {'session': session, 'device': data})
try:
ncs = nc.Session(data['hostname'], int(data['port']), ssh)
except Exception as e:
nc.setSchemaCallback(None)
return json.dumps({'success': False, 'code': 500, 'message': str(e)})
nc.setSchemaCallback(None)
if username not in sessions:
sessions[username] = {}
# use key (as hostname:port:session-id) to store the created NETCONF session
key = ncs.host + ":" + str(ncs.port) + ":" + ncs.id
sessions[username][key] = {}
sessions[username][key]['session'] = ncs
# update inventory's list of schemas
# schemas_update(session)
return json.dumps({'success': True, 'session-key': key})
def hostkey_check(hostname, state, keytype, hexa, priv):
if 'fingerprint' in priv['device']:
# check according to the stored fingerprint from previous connection
if hexa == priv['device']['fingerprint']:
return True
elif state != 2:
log.error("Incorrect host key state")
state = 2
# ask frontend/user for hostkey check
params = {'id': priv['session']['session_id'], 'hostname': hostname, 'state': state, 'keytype': keytype,
'hexa': hexa, 'device': priv['device']}
sio_emit('hostcheck', params)
result = False
timeout = Timeout(30)
try:
# wait for response from the frontend
data = sio_wait(priv['session']['session_id'])
result = data['result']
except Timeout:
# no response received within the timeout
log.info("socketio: hostcheck timeout.")
except KeyError:
# invalid response
log.error("socketio: invalid hostcheck_result received.")
finally:
# we have the response
sio_clean(priv['session']['session_id'])
timeout.cancel()
if result:
# store confirmed fingerprint for future connections
priv['device']['fingerprint'] = hexa
if 'id' in priv['device'].keys():
update_hexa(priv['device']['id'], hexa, netconf_coll)
else:
update_hexa_by_device(priv['device'], hexa, netconf_coll)
return result
""" SESSION HANDLING """
@auth.required()
def sessions_get_open():
"""
Get all open sessions belonging to the user
:return: Array of session keys and devices belonging to sessions. Device names will not be loaded.
"""
global sessions
session = auth.lookup(request.headers.get('lgui-Authorization', None))
username = str(session['user'].username)
if username in sessions:
result = []
for key in sessions[username].keys():
ncs = sessions[username][key]['session']
device = get_device_from_session_data(ncs.host, ncs.port, username, ncs.user, netconf_coll)
if device is not None:
result.append({'key': key, 'device': device})
return json.dumps(result)
else:
return json.dumps([])
@auth.required()
def session_alive(key):
global sessions
session = auth.lookup(request.headers.get('lgui-Authorization', None))
username = str(session['user'].username)
if not username in sessions:
sessions[username] = {}
if key in sessions[username]:
return json.dumps({'success': True, 'code': 200})
else:
return json.dumps({'success': False, 'code': 404, 'message': 'Session not found'})
@auth.required()
def session_destroy(key):
global sessions
session = auth.lookup(request.headers.get('lgui-Authorization', None))
username = str(session['user'].username)
if not username in sessions:
sessions[username] = {}
if key in sessions[username]:
del sessions[username][key]
return json.dumps({'success': True, 'code': 200})
else:
return json.dumps({'success': False, 'code': 404, 'message': 'Session not found'})
@auth.required()
def session_destroy_all():
global sessions
session = auth.lookup(request.headers.get('lgui-Authorization', None))
username = str(session['user'].username)
if username in sessions:
del sessions[username]
return json.dumps({'success': True, 'code': 200})
@auth.required()
def session_rpc_get():
"""
code 500: wrong argument
code 404: session invalid -> try reconnecting
code 410: connection gone -> remove session, try reconnecting
code 418: Error in processing netconf request (nothing to do with a teapot)
"""
global sessions
session = auth.lookup(request.headers.get('lgui-Authorization', None))
username = str(session['user'].username)
req = request.args.to_dict()
if 'key' not in req:
return json.dumps({'success': False, 'code': 500, 'message': 'Missing session key.'})
if 'recursive' not in req:
return json.dumps({'success': False, 'code': 500, 'message': 'Missing recursive flag.'})
if username not in sessions:
sessions[username] = {}
key = req['key']
if key not in sessions[username]:
return json.dumps({'success': False, 'code': 404, 'message': 'Invalid session key.'})
try:
sessions[username][key]['data'] = sessions[username][key]['session'].rpcGet()
except ConnectionError as e:
del sessions[username][key]
return json.dumps({'success': False, 'code': 410, 'message': str(e)})
except nc.ReplyError as e:
err_list = []
for err in e.args[0]:
err_list.append(str(err))
return json.dumps({'success': False, 'code': 418, 'message': str(e)})
if 'path' not in req:
return data_info_roots(sessions[username][key]['data'], True if req['recursive'] == 'true' else False)
else:
return data_info_subtree(sessions[username][key]['data'], req['path'],
True if req['recursive'] == 'true' else False)
@auth.required()
def session_commit():
session = auth.lookup(request.headers.get('lgui-Authorization', None))
user = session['user']
req = request.get_json(keep_order=True)
if 'key' not in req:
return json.dumps({'success': False, 'code': 500, 'message': 'Missing session key.'})
if 'modifications' not in req:
return json.dumps({'success': False, 'code': 500, 'message': 'Missing modifications.'})
mods = req['modifications']
ctx = sessions[user.username][req['key']]['session'].context
root = None
reorders = []
for key in mods:
recursion = False
# get correct path and value if needed
path = mods[key]['data']['path']
value = None
if mods[key]['type'] == 'change':
value = mods[key]['value']
elif mods[key]['type'] == 'create' or mods[key]['type'] == 'replace':
if mods[key]['data']['info']['type'] == 1:
# creating/replacing container
recursion = True
elif mods[key]['data']['info']['type'] == 4:
# creating/replacing leaf
value = mods[key]['data']['value']
elif mods[key]['data']['info']['type'] == 8:
# creating/replacing leaf-list
value = mods[key]['data']['value'][0]
path = mods[key]['data']['path']
elif mods[key]['data']['info']['type'] == 16:
recursion = True
path = mods[key]['data']['path']
elif mods[key]['type'] == 'reorder':
# postpone reorders
reorders.extend(mods[key]['transactions'])
continue
# create node
# print("creating " + path)
# print("value " + str(value))
if root:
root.new_path(ctx, path, value, 0, 0)
else:
try:
root = yang.Data_Node(ctx, path, value, 0, 0)
except Exception as e:
print(e)
return json.dumps({'success': False, 'code': 404, 'message': str(e)})
node = root.find_path(path).data()[0]
# set operation attribute and add additional data if any
if mods[key]['type'] == 'change':
node.insert_attr(None, 'ietf-netconf:operation', 'merge')
elif mods[key]['type'] == 'delete':
node.insert_attr(None, 'ietf-netconf:operation', 'delete')
elif mods[key]['type'] == 'create':
node.insert_attr(None, 'ietf-netconf:operation', 'create')
elif mods[key]['type'] == 'replace':
node.insert_attr(None, 'ietf-netconf:operation', 'replace')
else:
return json.dumps({'success': False, 'error-msg': 'Invalid modification ' + key})
if recursion and 'children' in mods[key]['data']:
for child in mods[key]['data']['children']:
if 'key' in child['info'] and child['info']['key']:
continue
_create_child(ctx, node, child)
# finally process reorders which must be last since they may refer newly created nodes
# and they do not reflect removed nodes
for move in reorders:
try:
node = root.find_path(move['node']).data()[0];
parent = node.parent()
node.unlink()
if parent:
parent.insert(node)
else:
root.insert_sibling(node)
except Exception as e:
if root:
root.new_path(ctx, move['node'], None, 0, 0)
else:
root = yang.Data_Node(ctx, move['node'], None, 0, 0)
node = root.find_path(move['node']).data()[0];
node.insert_attr(None, 'yang:insert', move['insert'])
if move['insert'] == 'after' or move['insert'] == 'before':
if 'key' in move:
node.insert_attr(None, 'yang:key', move['key'])
elif 'value' in move:
node.insert_attr(None, 'yang:value', move['value'])
# print(root.print_mem(yang.LYD_XML, yang.LYP_FORMAT))
try:
sessions[user.username][req['key']]['session'].rpcEditConfig(nc.DATASTORE_RUNNING, root)
except nc.ReplyError as e:
reply = {'success': False, 'code': 500, 'message': '[]'}
for err in e.args[0]:
reply['message'] += str(err) + '; '
return json.dumps(reply)
return json.dumps({'success': True})
def _create_child(ctx, parent, child_def):
at = child_def['info']['module'].find('@')
if at == -1:
module = ctx.get_module(child_def['info']['module'])
else:
module = ctx.get_module(child_def['info']['module'][:at], child_def['info']['module'][at + 1:])
if child_def['info']['type'] == 4:
yang.Data_Node(parent, module, child_def['info']['name'], child_def['value'])
elif child_def['info']['type'] == 8:
yang.Data_Node(parent, module, child_def['info']['name'], child_def['value'][0])
else:
child = yang.Data_Node(parent, module, child_def['info']['name'])
if 'children' in child_def:
for grandchild in child_def['children']:
_create_child(ctx, child, grandchild)