Page MenuHomeVyOS Platform

vyos-http-api-server
No OneTemporary

Size
10 KB
Referenced Files
None
Subscribers
None

vyos-http-api-server

#!/usr/bin/env python3
#
# Copyright (C) 2019 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
import os
import sys
import grp
import json
import traceback
import threading
import vyos.config
import bottle
from functools import wraps
from vyos.configsession import ConfigSession, ConfigSessionError
from vyos.config import VyOSError
DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf'
CFG_GROUP = 'vyattacfg'
app = bottle.default_app()
# Giant lock!
lock = threading.Lock()
def load_server_config():
with open(DEFAULT_CONFIG_FILE) as f:
config = json.load(f)
return config
def check_auth(key_list, key):
id = None
for k in key_list:
if k['key'] == key:
id = k['id']
return id
def error(code, msg):
bottle.response.status = code
resp = {"success": False, "error": msg, "data": None}
return json.dumps(resp)
def success(data):
resp = {"success": True, "data": data, "error": None}
return json.dumps(resp)
def auth_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
key = bottle.request.forms.get("key")
api_keys = app.config['vyos_keys']
id = check_auth(api_keys, key)
if not id:
return error(401, "Valid API key is required")
return f(*args, **kwargs)
return decorated_function
@app.route('/configure', method='POST')
@auth_required
def configure():
session = app.config['vyos_session']
env = session.get_session_env()
config = vyos.config.Config(session_env=env)
strict_field = bottle.request.forms.get("strict")
if strict_field == "true":
strict = True
else:
strict = False
commands = bottle.request.forms.get("data")
if not commands:
return error(400, "Non-empty data field is required")
else:
try:
commands = json.loads(commands)
except Exception as e:
return error(400, "Failed to parse JSON: {0}".format(e))
# Allow users to pass just one command
if not isinstance(commands, list):
commands = [commands]
# We don't want multiple people/apps to be able to commit at once,
# or modify the shared session while someone else is doing the same,
# so the lock is really global
lock.acquire()
status = 200
error_msg = None
try:
for c in commands:
# What we've got may not even be a dict
if not isinstance(c, dict):
raise ConfigSessionError("Malformed command \"{0}\": any command must be a dict".format(json.dumps(c)))
# Missing op or path is a show stopper
if not ('op' in c):
raise ConfigSessionError("Malformed command \"{0}\": missing \"op\" field".format(json.dumps(c)))
if not ('path' in c):
raise ConfigSessionError("Malformed command \"{0}\": missing \"path\" field".format(json.dumps(c)))
# Missing value is fine, substitute for empty string
if 'value' in c:
value = c['value']
else:
value = ""
op = c['op']
path = c['path']
if not path:
raise ConfigSessionError("Malformed command \"{0}\": empty path".format(json.dumps(c)))
# Type checking
if not isinstance(path, list):
raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list".format(json.dumps(c)))
if not isinstance(value, str):
raise ConfigSessionError("Malformed command \"{0}\": \"value\" field must be a string".format(json.dumps(c)))
# Account for the case when value field is present and set to null
if not value:
value = ""
# For vyos.configsessios calls that have no separate value arguments,
# and for type checking too
try:
cfg_path = " ".join(path + [value]).strip()
except TypeError:
raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list of strings".format(json.dumps(c)))
if op == 'set':
# XXX: it would be nice to do a strict check for "path already exists",
# but there's probably no way to do that
session.set(path, value=value)
elif op == 'delete':
if strict and not config.exists(cfg_path):
raise ConfigSessionError("Cannot delete [{0}]: path/value does not exist".format(cfg_path))
session.delete(path, value=value)
elif op == 'comment':
session.comment(path, value=value)
else:
raise ConfigSessionError("\"{0}\" is not a valid operation".format(op))
# end for
session.commit()
print("Configuration modified via HTTP API using key \"{0}\"".format(id))
except ConfigSessionError as e:
session.discard()
status = 400
if app.config['vyos_debug']:
print(traceback.format_exc(), file=sys.stderr)
error_msg = str(e)
except Exception as e:
session.discard()
print(traceback.format_exc(), file=sys.stderr)
status = 500
# Don't give the details away to the outer world
error_msg = "An internal error occured. Check the logs for details."
finally:
lock.release()
if status != 200:
return error(status, error_msg)
else:
return success(None)
@app.route('/retrieve', method='POST')
@auth_required
def get_value():
session = app.config['vyos_session']
env = session.get_session_env()
config = vyos.config.Config(session_env=env)
command = bottle.request.forms.get("data")
command = json.loads(command)
try:
op = command['op']
path = " ".join(command['path'])
except KeyError:
return error(400, "Missing required field. \"op\" and \"path\" fields are required")
try:
if op == 'returnValue':
res = config.return_value(path)
elif op == 'returnValues':
res = config.return_values(path)
elif op == 'exists':
res = config.exists(path)
elif op == 'showConfig':
config_format = 'json'
if 'configFormat' in command:
config_format = command['configFormat']
res = session.show_config(path=command['path'])
if config_format == 'json':
config_tree = vyos.configtree.ConfigTree(res)
res = json.loads(config_tree.to_json())
elif config_format == 'json_ast':
config_tree = vyos.configtree.ConfigTree(res)
res = json.loads(config_tree.to_json_ast())
elif config_format == 'raw':
pass
else:
return error(400, "\"{0}\" is not a valid config format".format(config_format))
else:
return error(400, "\"{0}\" is not a valid operation".format(op))
except VyOSError as e:
return error(400, str(e))
except Exception as e:
print(traceback.format_exc(), file=sys.stderr)
return error(500, "An internal error occured. Check the logs for details.")
return success(res)
@app.route('/config-file', method='POST')
@auth_required
def config_file_op():
session = app.config['vyos_session']
command = bottle.request.forms.get("data")
command = json.loads(command)
try:
op = command['op']
except KeyError:
return error(400, "Missing required field \"op\"")
try:
if op == 'save':
try:
path = command['file']
except KeyError:
path = '/config/config.boot'
res = session.save_config(path)
elif op == 'load':
try:
path = command['file']
except KeyError:
return error(400, "Missing required field \"file\"")
res = session.load_config(path)
res = session.commit()
else:
return error(400, "\"{0}\" is not a valid operation".format(op))
except VyOSError as e:
return error(400, str(e))
except Exception as e:
print(traceback.format_exc(), file=sys.stderr)
return error(500, "An internal error occured. Check the logs for details.")
return success(res)
@app.route('/image', method='POST')
@auth_required
def config_file_op():
session = app.config['vyos_session']
command = bottle.request.forms.get("data")
command = json.loads(command)
try:
op = command['op']
except KeyError:
return error(400, "Missing required field \"op\"")
try:
if op == 'add':
try:
url = command['url']
except KeyError:
return error(400, "Missing required field \"url\"")
res = session.install_image(url)
elif op == 'delete':
try:
name = command['name']
except KeyError:
return error(400, "Missing required field \"name\"")
res = session.remove_image(name)
else:
return error(400, "\"{0}\" is not a valid operation".format(op))
except VyOSError as e:
return error(400, str(e))
except Exception as e:
print(traceback.format_exc(), file=sys.stderr)
return error(500, "An internal error occured. Check the logs for details.")
return success(res)
if __name__ == '__main__':
# systemd's user and group options don't work, do it by hand here,
# else no one else will be able to commit
cfg_group = grp.getgrnam(CFG_GROUP)
os.setgid(cfg_group.gr_gid)
# Need to set file permissions to 775 too so that every vyattacfg group member
# has write access to the running config
os.umask(0o002)
try:
server_config = load_server_config()
except Exception as e:
print("Failed to load the HTTP API server config: {0}".format(e))
session = ConfigSession(os.getpid())
app.config['vyos_session'] = session
app.config['vyos_keys'] = server_config['api_keys']
app.config['vyos_debug'] = server_config['debug']
bottle.run(app, host=server_config["listen_address"], port=server_config["port"], debug=True)

File Metadata

Mime Type
text/x-script.python
Expires
Sat, Oct 17, 12:14 AM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4374696
Default Alt Text
vyos-http-api-server (10 KB)

Event Timeline