diff options
Diffstat (limited to 'src/services')
| -rwxr-xr-x | src/services/vyos-hostsd | 618 | ||||
| -rwxr-xr-x | src/services/vyos-http-api-server | 400 |
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}") |
