summaryrefslogtreecommitdiff
path: root/src/services
diff options
context:
space:
mode:
Diffstat (limited to 'src/services')
-rwxr-xr-xsrc/services/vyos-hostsd618
-rwxr-xr-xsrc/services/vyos-http-api-server400
2 files changed, 1018 insertions, 0 deletions
diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd
new file mode 100755
index 000000000..0079f7e5c
--- /dev/null
+++ b/src/services/vyos-hostsd
@@ -0,0 +1,618 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2020 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/>.
+#
+#########
+# USAGE #
+#########
+# This daemon listens on its socket for JSON messages.
+# The received message format is:
+#
+# { 'type': '<message type>',
+# 'op': '<message operation>',
+# 'data': <data list or dict>
+# }
+#
+# For supported message types, see below.
+# 'op' can be 'add', delete', 'get', 'set' or 'apply'.
+# Different message types support different sets of operations and different
+# data formats.
+#
+# Changes to configuration made via add or delete don't take effect immediately,
+# they are remembered in a state variable and saved to disk to a state file.
+# State is remembered across daemon restarts but not across system reboots
+# as it's saved in a temporary filesystem (/run).
+#
+# 'apply' is a special operation that applies the configuration from the cached
+# state, rendering all config files and reloading relevant daemons (currently
+# just pdns-recursor via rec-control).
+#
+# note: 'add' operation also acts as 'update' as it uses dict.update, if the
+# 'data' dict item value is a dict. If it is a list, it uses list.append.
+#
+### tags
+# Tags can be arbitrary, but they are generally in this format:
+# 'static', 'system', 'dhcp(v6)-<intf>' or 'dhcp-server-<client ip>'
+# They are used to distinguish entries created by different scripts so they can
+# be removed and recreated without having to track what needs to be changed.
+# They are also used as a way to control which tags settings (e.g. nameservers)
+# get added to various config files via name_server_tags_(recursor|system)
+#
+### name_server_tags_(recursor|system)
+# A list of tags whose nameservers and search domains is used to generate
+# /etc/resolv.conf and pdns-recursor config.
+# system list is used to generate resolv.conf.
+# recursor list is used to generate pdns-rec forward-zones.
+# When generating each file, the order of nameservers is as per the order of
+# name_server_tags (the order in which tags were added), then the order in
+# which the name servers for each tag were added.
+#
+#### Message types
+#
+### name_servers
+#
+# { 'type': 'name_servers',
+# 'op': 'add',
+# 'data': {
+# '<str tag>': ['<str nameserver>', ...],
+# ...
+# }
+# }
+#
+# { 'type': 'name_servers',
+# 'op': 'delete',
+# 'data': ['<str tag>', ...]
+# }
+#
+# { 'type': 'name_servers',
+# 'op': 'get',
+# 'tag_regex': '<str regex>'
+# }
+# response:
+# { 'data': {
+# '<str tag>': ['<str nameserver>', ...],
+# ...
+# }
+# }
+#
+### name_server_tags
+#
+# { 'type': 'name_server_tags',
+# 'op': 'add',
+# 'data': ['<str tag>', ...]
+# }
+#
+# { 'type': 'name_server_tags',
+# 'op': 'delete',
+# 'data': ['<str tag>', ...]
+# }
+#
+# { 'type': 'name_server_tags',
+# 'op': 'get',
+# }
+# response:
+# { 'data': ['<str tag>', ...] }
+#
+### forward_zones
+## Additional zones added to pdns-recursor forward-zones-file.
+## If recursion-desired is true, '+' will be prepended to the zone line.
+## If addNTA is true, a NTA will be added via lua-config-file.
+#
+# { 'type': 'forward_zones',
+# 'op': 'add',
+# 'data': {
+# '<str zone>': {
+# 'nslist': ['<str nameserver>', ...],
+# 'addNTA': <bool>,
+# 'recursion-desired': <bool>
+# }
+# ...
+# }
+# }
+#
+# { 'type': 'forward_zones',
+# 'op': 'delete',
+# 'data': ['<str zone>', ...]
+# }
+#
+# { 'type': 'forward_zones',
+# 'op': 'get',
+# }
+# response:
+# { 'data': {
+# '<str zone>': { ... },
+# ...
+# }
+# }
+#
+#
+### search_domains
+#
+# { 'type': 'search_domains',
+# 'op': 'add',
+# 'data': {
+# '<str tag>': ['<str domain>', ...],
+# ...
+# }
+# }
+#
+# { 'type': 'search_domains',
+# 'op': 'delete',
+# 'data': ['<str tag>', ...]
+# }
+#
+# { 'type': 'search_domains',
+# 'op': 'get',
+# }
+# response:
+# { 'data': {
+# '<str tag>': ['<str domain>', ...],
+# ...
+# }
+# }
+#
+### hosts
+#
+# { 'type': 'hosts',
+# 'op': 'add',
+# 'data': {
+# '<str tag>': {
+# '<str host>': {
+# 'address': '<str address>',
+# 'aliases': ['<str alias>, ...]
+# },
+# ...
+# },
+# ...
+# }
+# }
+#
+# { 'type': 'hosts',
+# 'op': 'delete',
+# 'data': ['<str tag>', ...]
+# }
+#
+# { 'type': 'hosts',
+# 'op': 'get'
+# 'tag_regex': '<str regex>'
+# }
+# response:
+# { 'data': {
+# '<str tag>': {
+# '<str host>': {
+# 'address': '<str address>',
+# 'aliases': ['<str alias>, ...]
+# },
+# ...
+# },
+# ...
+# }
+# }
+### host_name
+#
+# { 'type': 'host_name',
+# 'op': 'set',
+# 'data': {
+# 'host_name': '<str hostname>'
+# 'domain_name': '<str domainname>'
+# }
+# }
+
+import os
+import sys
+import time
+import json
+import signal
+import traceback
+import re
+import logging
+import zmq
+from voluptuous import Schema, MultipleInvalid, Required, Any
+from collections import OrderedDict
+from vyos.util import popen, chown, chmod_755, makedir, process_named_running
+from vyos.template import render
+
+debug = True
+
+# Configure logging
+logger = logging.getLogger(__name__)
+# set stream as output
+logs_handler = logging.StreamHandler()
+logger.addHandler(logs_handler)
+
+if debug:
+ logger.setLevel(logging.DEBUG)
+else:
+ logger.setLevel(logging.INFO)
+
+RUN_DIR = "/run/vyos-hostsd"
+STATE_FILE = os.path.join(RUN_DIR, "vyos-hostsd.state")
+SOCKET_PATH = "ipc://" + os.path.join(RUN_DIR, 'vyos-hostsd.sock')
+
+RESOLV_CONF_FILE = '/etc/resolv.conf'
+HOSTS_FILE = '/etc/hosts'
+
+PDNS_REC_USER = PDNS_REC_GROUP = 'pdns'
+PDNS_REC_RUN_DIR = '/run/powerdns'
+PDNS_REC_LUA_CONF_FILE = f'{PDNS_REC_RUN_DIR}/recursor.vyos-hostsd.conf.lua'
+PDNS_REC_ZONES_FILE = f'{PDNS_REC_RUN_DIR}/recursor.forward-zones.conf'
+
+STATE = {
+ "name_servers": {},
+ "name_server_tags_recursor": [],
+ "name_server_tags_system": [],
+ "forward_zones": {},
+ "hosts": {},
+ "host_name": "vyos",
+ "domain_name": "",
+ "search_domains": {},
+ "changes": 0
+ }
+
+# the base schema that every received message must be in
+base_schema = Schema({
+ Required('op'): Any('add', 'delete', 'set', 'get', 'apply'),
+ 'type': Any('name_servers',
+ 'name_server_tags_recursor', 'name_server_tags_system',
+ 'forward_zones', 'search_domains', 'hosts', 'host_name'),
+ 'data': Any(list, dict),
+ 'tag': str,
+ 'tag_regex': str
+ })
+
+# more specific schemas
+op_schema = Schema({
+ 'op': str,
+ }, required=True)
+
+op_type_schema = op_schema.extend({
+ 'type': str,
+ }, required=True)
+
+host_name_add_schema = op_type_schema.extend({
+ 'data': {
+ 'host_name': str,
+ 'domain_name': Any(str, None)
+ }
+ }, required=True)
+
+data_dict_list_schema = op_type_schema.extend({
+ 'data': {
+ str: [str]
+ }
+ }, required=True)
+
+data_list_schema = op_type_schema.extend({
+ 'data': [str]
+ }, required=True)
+
+tag_regex_schema = op_type_schema.extend({
+ 'tag_regex': str
+ }, required=True)
+
+forward_zone_add_schema = op_type_schema.extend({
+ 'data': {
+ str: {
+ 'nslist': [str],
+ 'addNTA': bool,
+ 'recursion-desired': bool
+ }
+ }
+ }, required=True)
+
+hosts_add_schema = op_type_schema.extend({
+ 'data': {
+ str: {
+ str: {
+ 'address': str,
+ 'aliases': [str]
+ }
+ }
+ }
+ }, required=True)
+
+
+# op and type to schema mapping
+msg_schema_map = {
+ 'name_servers': {
+ 'add': data_dict_list_schema,
+ 'delete': data_list_schema,
+ 'get': tag_regex_schema
+ },
+ 'name_server_tags_recursor': {
+ 'add': data_list_schema,
+ 'delete': data_list_schema,
+ 'get': op_type_schema
+ },
+ 'name_server_tags_system': {
+ 'add': data_list_schema,
+ 'delete': data_list_schema,
+ 'get': op_type_schema
+ },
+ 'forward_zones': {
+ 'add': forward_zone_add_schema,
+ 'delete': data_list_schema,
+ 'get': op_type_schema
+ },
+ 'search_domains': {
+ 'add': data_dict_list_schema,
+ 'delete': data_list_schema,
+ 'get': tag_regex_schema
+ },
+ 'hosts': {
+ 'add': hosts_add_schema,
+ 'delete': data_list_schema,
+ 'get': tag_regex_schema
+ },
+ 'host_name': {
+ 'set': host_name_add_schema
+ },
+ None: {
+ 'apply': op_schema
+ }
+ }
+
+def validate_schema(data):
+ base_schema(data)
+
+ try:
+ schema = msg_schema_map[data['type'] if 'type' in data else None][data['op']]
+ schema(data)
+ except KeyError:
+ raise ValueError((
+ 'Invalid or unknown combination: '
+ f'op: "{data["op"]}", type: "{data["type"]}"'))
+
+
+def pdns_rec_control(command):
+ # pdns-r process name is NOT equal to the name shown in ps
+ if not process_named_running('pdns-r/worker'):
+ logger.info(f'pdns_recursor not running, not sending "{command}"')
+ return
+
+ logger.info(f'Running "rec_control {command}"')
+ (ret,ret_code) = popen((
+ f"rec_control --socket-dir={PDNS_REC_RUN_DIR} {command}"))
+ if ret_code > 0:
+ logger.exception((
+ f'"rec_control {command}" failed with exit status {ret_code}, '
+ f'output: "{ret}"'))
+
+def make_resolv_conf(state):
+ logger.info(f"Writing {RESOLV_CONF_FILE}")
+ render(RESOLV_CONF_FILE, 'vyos-hostsd/resolv.conf.tmpl', state,
+ user='root', group='root')
+
+def make_hosts(state):
+ logger.info(f"Writing {HOSTS_FILE}")
+ render(HOSTS_FILE, 'vyos-hostsd/hosts.tmpl', state,
+ user='root', group='root')
+
+def make_pdns_rec_conf(state):
+ logger.info(f"Writing {PDNS_REC_LUA_CONF_FILE}")
+
+ # on boot, /run/powerdns does not exist, so create it
+ makedir(PDNS_REC_RUN_DIR, user=PDNS_REC_USER, group=PDNS_REC_GROUP)
+ chmod_755(PDNS_REC_RUN_DIR)
+
+ render(PDNS_REC_LUA_CONF_FILE,
+ 'dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl',
+ state, user=PDNS_REC_USER, group=PDNS_REC_GROUP)
+
+ logger.info(f"Writing {PDNS_REC_ZONES_FILE}")
+ render(PDNS_REC_ZONES_FILE,
+ 'dns-forwarding/recursor.forward-zones.conf.tmpl',
+ state, user=PDNS_REC_USER, group=PDNS_REC_GROUP)
+
+def set_host_name(state, data):
+ if data['host_name']:
+ state['host_name'] = data['host_name']
+ if 'domain_name' in data:
+ state['domain_name'] = data['domain_name']
+
+def add_items_to_dict(_dict, items):
+ """
+ Dedupes and preserves sort order.
+ """
+ assert isinstance(_dict, dict)
+ assert isinstance(items, dict)
+
+ if not items:
+ return
+
+ _dict.update(items)
+
+def add_items_to_dict_as_keys(_dict, items):
+ """
+ Added item values are converted to OrderedDict with the value as keys
+ and null values. This is to emulate a list but with inherent deduplication.
+ Dedupes and preserves sort order.
+ """
+ assert isinstance(_dict, dict)
+ assert isinstance(items, dict)
+
+ if not items:
+ return
+
+ for item, item_val in items.items():
+ if item not in _dict:
+ _dict[item] = OrderedDict({})
+ _dict[item].update(OrderedDict.fromkeys(item_val))
+
+def add_items_to_list(_list, items):
+ """
+ Dedupes and preserves sort order.
+ """
+ assert isinstance(_list, list)
+ assert isinstance(items, list)
+
+ if not items:
+ return
+
+ for item in items:
+ if item not in _list:
+ _list.append(item)
+
+def delete_items_from_dict(_dict, items):
+ """
+ items is a list of keys to delete.
+ Doesn't error if the key doesn't exist.
+ """
+ assert isinstance(_dict, dict)
+ assert isinstance(items, list)
+
+ for item in items:
+ if item in _dict:
+ del _dict[item]
+
+def delete_items_from_list(_list, items):
+ """
+ items is a list of items to remove.
+ Doesn't error if the key doesn't exist.
+ """
+ assert isinstance(_list, list)
+ assert isinstance(items, list)
+
+ for item in items:
+ if item in _list:
+ _list.remove(item)
+
+def get_items_from_dict_regex(_dict, item_regex_string):
+ """
+ Returns the items whose keys match item_regex_string.
+ """
+ assert isinstance(_dict, dict)
+ assert isinstance(item_regex_string, str)
+
+ tmp = {}
+ regex = re.compile(item_regex_string)
+ for item in _dict:
+ if regex.match(item):
+ tmp[item] = _dict[item]
+ return tmp
+
+def get_option(msg, key):
+ if key in msg:
+ return msg[key]
+ else:
+ raise ValueError("Missing required option \"{0}\"".format(key))
+
+def handle_message(msg):
+ result = None
+ op = get_option(msg, 'op')
+
+ if op in ['add', 'delete', 'set']:
+ STATE['changes'] += 1
+
+ if op == 'delete':
+ _type = get_option(msg, 'type')
+ data = get_option(msg, 'data')
+ if _type in ['name_servers', 'forward_zones', 'search_domains', 'hosts']:
+ delete_items_from_dict(STATE[_type], data)
+ elif _type in ['name_server_tags_recursor', 'name_server_tags_system']:
+ delete_items_from_list(STATE[_type], data)
+ else:
+ raise ValueError(f'Operation "{op}" unknown data type "{_type}"')
+ elif op == 'add':
+ _type = get_option(msg, 'type')
+ data = get_option(msg, 'data')
+ if _type in ['name_servers', 'search_domains']:
+ add_items_to_dict_as_keys(STATE[_type], data)
+ elif _type in ['forward_zones', 'hosts']:
+ add_items_to_dict(STATE[_type], data)
+ # maybe we need to rec_control clear-nta each domain that was removed here?
+ elif _type in ['name_server_tags_recursor', 'name_server_tags_system']:
+ add_items_to_list(STATE[_type], data)
+ else:
+ raise ValueError(f'Operation "{op}" unknown data type "{_type}"')
+ elif op == 'set':
+ _type = get_option(msg, 'type')
+ data = get_option(msg, 'data')
+ if _type == 'host_name':
+ set_host_name(STATE, data)
+ else:
+ raise ValueError(f'Operation "{op}" unknown data type "{_type}"')
+ elif op == 'get':
+ _type = get_option(msg, 'type')
+ if _type in ['name_servers', 'search_domains', 'hosts']:
+ tag_regex = get_option(msg, 'tag_regex')
+ result = get_items_from_dict_regex(STATE[_type], tag_regex)
+ elif _type in ['name_server_tags_recursor', 'name_server_tags_system', 'forward_zones']:
+ result = STATE[_type]
+ else:
+ raise ValueError(f'Operation "{op}" unknown data type "{_type}"')
+ elif op == 'apply':
+ logger.info(f"Applying {STATE['changes']} changes")
+ make_resolv_conf(STATE)
+ make_hosts(STATE)
+ make_pdns_rec_conf(STATE)
+ pdns_rec_control('reload-lua-config')
+ pdns_rec_control('reload-zones')
+ logger.info("Success")
+ result = {'message': f'Applied {STATE["changes"]} changes'}
+ STATE['changes'] = 0
+
+ else:
+ raise ValueError(f"Unknown operation {op}")
+
+ logger.debug(f"Saving state to {STATE_FILE}")
+ with open(STATE_FILE, 'w') as f:
+ json.dump(STATE, f)
+
+ return result
+
+if __name__ == '__main__':
+ # Create a directory for state checkpoints
+ os.makedirs(RUN_DIR, exist_ok=True)
+ if os.path.exists(STATE_FILE):
+ with open(STATE_FILE, 'r') as f:
+ try:
+ STATE = json.load(f)
+ except:
+ logger.exception(traceback.format_exc())
+ logger.exception("Failed to load the state file, using default")
+
+ context = zmq.Context()
+ socket = context.socket(zmq.REP)
+
+ # Set the right permissions on the socket, then change it back
+ o_mask = os.umask(0o007)
+ socket.bind(SOCKET_PATH)
+ os.umask(o_mask)
+
+ while True:
+ # Wait for next request from client
+ msg_json = socket.recv().decode()
+ logger.debug(f"Request data: {msg_json}")
+
+ try:
+ msg = json.loads(msg_json)
+ validate_schema(msg)
+
+ resp = {}
+ resp['data'] = handle_message(msg)
+ except ValueError as e:
+ resp['error'] = str(e)
+ except MultipleInvalid as e:
+ # raised by schema
+ resp['error'] = f'Invalid message: {str(e)}'
+ logger.exception(resp['error'])
+ except:
+ logger.exception(traceback.format_exc())
+ resp['error'] = "Internal error"
+
+ # Send reply back to client
+ socket.send(json.dumps(resp).encode())
+ logger.debug(f"Sent response: {resp}")
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
new file mode 100755
index 000000000..d5730d86c
--- /dev/null
+++ b/src/services/vyos-http-api-server
@@ -0,0 +1,400 @@
+#!/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 signal
+
+import vyos.config
+
+from flask import Flask, request
+from waitress import serve
+
+from functools import wraps
+
+from vyos.configsession import ConfigSession, ConfigSessionError
+
+
+DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf'
+CFG_GROUP = 'vyattacfg'
+
+app = Flask(__name__)
+
+# 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):
+ resp = {"success": False, "error": msg, "data": None}
+ return json.dumps(resp), code
+
+def success(data):
+ resp = {"success": True, "data": data, "error": None}
+ return json.dumps(resp)
+
+def get_command(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ cmd = request.form.get("data")
+ if not cmd:
+ return error(400, "Non-empty data field is required")
+ try:
+ cmd = json.loads(cmd)
+ except Exception as e:
+ return error(400, "Failed to parse JSON: {0}".format(e))
+ return f(cmd, *args, **kwargs)
+
+ return decorated_function
+
+def auth_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ key = request.form.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', methods=['POST'])
+@get_command
+@auth_required
+def configure_op(commands):
+ session = app.config['vyos_session']
+ env = session.get_session_env()
+ config = vyos.config.Config(session_env=env)
+
+ strict_field = request.form.get("strict")
+ if strict_field == "true":
+ strict = True
+ else:
+ strict = False
+
+ # 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', methods=['POST'])
+@get_command
+@auth_required
+def retrieve_op(command):
+ session = app.config['vyos_session']
+ env = session.get_session_env()
+ config = vyos.config.Config(session_env=env)
+
+ 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 ConfigSessionError 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', methods=['POST'])
+@get_command
+@auth_required
+def config_file_op(command):
+ session = app.config['vyos_session']
+
+ 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 ConfigSessionError 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', methods=['POST'])
+@get_command
+@auth_required
+def image_op(command):
+ session = app.config['vyos_session']
+
+ 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 ConfigSessionError 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('/generate', methods=['POST'])
+@get_command
+@auth_required
+def generate_op(command):
+ session = app.config['vyos_session']
+
+ try:
+ op = command['op']
+ path = command['path']
+ except KeyError:
+ return error(400, "Missing required field. \"op\" and \"path\" fields are required")
+
+ if not isinstance(path, list):
+ return error(400, "Malformed command: \"path\" field must be a list of strings")
+
+ try:
+ if op == 'generate':
+ res = session.generate(path)
+ else:
+ return error(400, "\"{0}\" is not a valid operation".format(op))
+ except ConfigSessionError 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('/show', methods=['POST'])
+@get_command
+@auth_required
+def show_op(command):
+ session = app.config['vyos_session']
+
+ try:
+ op = command['op']
+ path = command['path']
+ except KeyError:
+ return error(400, "Missing required field. \"op\" and \"path\" fields are required")
+
+ if not isinstance(path, list):
+ return error(400, "Malformed command: \"path\" field must be a list of strings")
+
+ try:
+ if op == 'show':
+ res = session.show(path)
+ else:
+ return error(400, "\"{0}\" is not a valid operation".format(op))
+ except ConfigSessionError 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)
+
+def shutdown():
+ raise KeyboardInterrupt
+
+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']
+
+ def sig_handler(signum, frame):
+ shutdown()
+
+ signal.signal(signal.SIGTERM, sig_handler)
+
+ try:
+ serve(app, host=server_config["listen_address"],
+ port=server_config["port"])
+ except OSError as e:
+ print(f"OSError {e}")