summaryrefslogtreecommitdiff
path: root/src/services/vyos-http-api-server
diff options
context:
space:
mode:
authorDaniil Baturin <daniil@baturin.org>2019-09-04 15:39:38 +0200
committerDaniil Baturin <daniil@baturin.org>2019-09-04 15:39:38 +0200
commit4d50edfc9543f3d27eb83300dd27d598ffe63fe2 (patch)
tree6d403977484aa5231659d17efdd031ea01c1a768 /src/services/vyos-http-api-server
parent6167fab1737ef59f223ee6c7fff0a493c76d9793 (diff)
downloadvyos-1x-4d50edfc9543f3d27eb83300dd27d598ffe63fe2.tar.gz
vyos-1x-4d50edfc9543f3d27eb83300dd27d598ffe63fe2.zip
T1443: backport the HTTP API to crux.
Implementation by Daniil Baturin and John Estabrook.
Diffstat (limited to 'src/services/vyos-http-api-server')
-rwxr-xr-xsrc/services/vyos-http-api-server247
1 files changed, 247 insertions, 0 deletions
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
new file mode 100755
index 000000000..afab9be70
--- /dev/null
+++ b/src/services/vyos-http-api-server
@@ -0,0 +1,247 @@
+#!/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 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)
+
+@app.route('/configure', method='POST')
+def configure():
+ session = app.config['vyos_session']
+ config = app.config['vyos_config']
+ api_keys = app.config['vyos_keys']
+
+ key = bottle.request.forms.get("key")
+ id = check_auth(api_keys, key)
+ if not id:
+ return error(401, "Valid API key is required")
+
+ 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')
+def get_value():
+ config = app.config['vyos_config']
+ session = app.config['vyos_session']
+
+ api_keys = app.config['vyos_keys']
+
+ key = bottle.request.forms.get("key")
+ id = check_auth(api_keys, key)
+ if not id:
+ return error(401, "Valid API key is required")
+
+ 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 = 'raw'
+ if 'configFormat' in command:
+ config_format = command['configFormat']
+
+ res = session.show_config(command['path'], 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)
+
+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())
+ env = session.get_session_env()
+ config = vyos.config.Config(session_env=env)
+
+ app.config['vyos_session'] = session
+ app.config['vyos_config'] = config
+ 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)