summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniil Baturin <daniil@baturin.org>2019-06-16 21:26:38 +0200
committerDaniil Baturin <daniil@baturin.org>2019-06-16 21:26:38 +0200
commitfc2cc0f9b660408d5fc0cffcaffc33bfbc8ca5f2 (patch)
tree3e560d1d3ab7d587b8fc53147c6985972b3bea96
parent9bf7d03ff7342e7f87710df6bcc15beceed9582c (diff)
downloadvyos-1x-fc2cc0f9b660408d5fc0cffcaffc33bfbc8ca5f2.tar.gz
vyos-1x-fc2cc0f9b660408d5fc0cffcaffc33bfbc8ca5f2.zip
T1431: initial implementation of the HTTP API.
-rwxr-xr-xdebian/rules5
-rwxr-xr-xsrc/services/vyos-http-api-server203
-rw-r--r--src/systemd/vyos-http-api.service16
3 files changed, 224 insertions, 0 deletions
diff --git a/debian/rules b/debian/rules
index ff2d205ba..b06117922 100755
--- a/debian/rules
+++ b/debian/rules
@@ -10,6 +10,7 @@ VYOS_OP_TMPL_DIR := /opt/vyatta/share/vyatta-op/templates
MIGRATION_SCRIPTS_DIR := /opt/vyatta/etc/config-migrate/migrate/
SYSTEM_SCRIPTS_DIR := usr/libexec/vyos/system
+SERVICES_DIR := usr/libexec/vyos/services
%:
dh $@ --with python3, --with quilt
@@ -53,6 +54,10 @@ override_dh_auto_install:
mkdir -p $(DIR)/$(SYSTEM_SCRIPTS_DIR)
cp -r src/system/* $(DIR)/$(SYSTEM_SCRIPTS_DIR)
+ # Install system services
+ mkdir -p $(DIR)/$(SERVICES_DIR)
+ cp -r src/services/* $(DIR)/$(SERVICES_DIR)
+
# Install configuration command definitions
mkdir -p $(DIR)/$(VYOS_CFG_TMPL_DIR)
cp -r templates-cfg/* $(DIR)/$(VYOS_CFG_TMPL_DIR)
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
new file mode 100755
index 000000000..32f8adc73
--- /dev/null
+++ b/src/services/vyos-http-api-server
@@ -0,0 +1,203 @@
+#!/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))
+
+ # 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:
+ op = c['op']
+ path = c['path']
+ value = c['value']
+
+ # Account for null values
+ if not value:
+ value = ""
+
+ # For vyos.config calls
+ cfg_path = " ".join(path + [value]).strip()
+
+ 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."
+
+ 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']
+
+ 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)
+
+ op = command['op']
+ path = " ".join(command['path'])
+
+ try:
+ if op == 'returnValue':
+ res = config.return_value(path)
+ elif op == 'returnValues':
+ res = config.return_values(path)
+ elif op == 'exists':
+ res = config.exists(path)
+ 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)
diff --git a/src/systemd/vyos-http-api.service b/src/systemd/vyos-http-api.service
new file mode 100644
index 000000000..f0665e3d5
--- /dev/null
+++ b/src/systemd/vyos-http-api.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=VyOS HTTP API service
+After=auditd.service systemd-user-sessions.service time-sync.target
+
+[Service]
+ExecStart=/usr/libexec/vyos/services/vyos-http-api-server
+ExecReload=/bin/kill -TERM $MAINPID
+KillMode=process
+
+# Does't work but leave it here
+User=root
+Group=vyattacfg
+
+[Install]
+WantedBy=multi-user.target
+