summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/http-api.py6
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py5
-rwxr-xr-xsrc/conf_mode/protocols_isis.py37
-rw-r--r--src/etc/sysctl.d/30-vyos-router.conf3
-rwxr-xr-xsrc/helpers/config_dependency.py79
-rwxr-xr-xsrc/helpers/vyos-save-config.py19
-rwxr-xr-xsrc/init/vyos-router26
-rwxr-xr-xsrc/services/vyos-http-api-server173
8 files changed, 282 insertions, 66 deletions
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index 793a90d88..d8fe3b736 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -27,6 +27,7 @@ from vyos.config import Config
from vyos.configdep import set_dependents, call_dependents
from vyos.template import render
from vyos.utils.process import call
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -130,7 +131,10 @@ def apply(http_api):
service_name = 'vyos-http-api.service'
if http_api is not None:
- call(f'systemctl restart {service_name}')
+ if is_systemd_service_running(f'{service_name}'):
+ call(f'systemctl reload {service_name}')
+ else:
+ call(f'systemctl restart {service_name}')
else:
call(f'systemctl stop {service_name}')
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index fca91253c..0a03a172c 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -77,6 +77,11 @@ def verify(pppoe):
if {'connect_on_demand', 'vrf'} <= set(pppoe):
raise ConfigError('On-demand dialing and VRF can not be used at the same time')
+ # both MTU and MRU have default values, thus we do not need to check
+ # if the key exists
+ if int(pppoe['mru']) > int(pppoe['mtu']):
+ raise ConfigError('PPPoE MRU needs to be lower then MTU!')
+
return None
def generate(pppoe):
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index e00c58ee4..ce67ccff7 100755
--- a/src/conf_mode/protocols_isis.py
+++ b/src/conf_mode/protocols_isis.py
@@ -48,7 +48,8 @@ def get_config(config=None):
# eqivalent of the C foo ? 'a' : 'b' statement
base = vrf and ['vrf', 'name', vrf, 'protocols', 'isis'] or base_path
isis = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True)
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
# Assign the name of our VRF context. This MUST be done before the return
# statement below, else on deletion we will delete the default instance
@@ -219,6 +220,38 @@ def verify(isis):
if ("explicit_null" in prefix_config['index']) and ("no_php_flag" in prefix_config['index']):
raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\
f'and no-php-flag configured at the same time.')
+
+ # Check for LFA tiebreaker index duplication
+ if dict_search('fast_reroute.lfa.local.tiebreaker', isis):
+ comparison_dictionary = {}
+ for item, item_options in isis['fast_reroute']['lfa']['local']['tiebreaker'].items():
+ for index, index_options in item_options.items():
+ for index_value, index_value_options in index_options.items():
+ if index_value not in comparison_dictionary.keys():
+ comparison_dictionary[index_value] = [item]
+ else:
+ comparison_dictionary[index_value].append(item)
+ for index, index_length in comparison_dictionary.items():
+ if int(len(index_length)) > 1:
+ raise ConfigError(f'LFA index {index} cannot have more than one tiebreaker configured.')
+
+ # Check for LFA priority-limit configured multiple times per level
+ if dict_search('fast_reroute.lfa.local.priority_limit', isis):
+ comparison_dictionary = {}
+ for priority, priority_options in isis['fast_reroute']['lfa']['local']['priority_limit'].items():
+ for level, level_options in priority_options.items():
+ if level not in comparison_dictionary.keys():
+ comparison_dictionary[level] = [priority]
+ else:
+ comparison_dictionary[level].append(priority)
+ for level, level_length in comparison_dictionary.items():
+ if int(len(level_length)) > 1:
+ raise ConfigError(f'LFA priority-limit on {level.replace("_", "-")} cannot have more than one priority configured.')
+
+ # Check for LFA remote prefix list configured with more than one list
+ if dict_search('fast_reroute.lfa.remote.prefix_list', isis):
+ if int(len(isis['fast_reroute']['lfa']['remote']['prefix_list'].items())) > 1:
+ raise ConfigError(f'LFA remote prefix-list has more than one configured. Cannot have more than one configured.')
return None
@@ -265,4 +298,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- exit(1)
+ exit(1) \ No newline at end of file
diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf
index fcdc1b21d..1c9b8999f 100644
--- a/src/etc/sysctl.d/30-vyos-router.conf
+++ b/src/etc/sysctl.d/30-vyos-router.conf
@@ -21,7 +21,6 @@ net.ipv4.conf.all.arp_filter=0
# https://vyos.dev/T300
net.ipv4.conf.all.arp_ignore=0
-
net.ipv4.conf.all.arp_announce=2
# Enable packet forwarding for IPv4
@@ -103,6 +102,6 @@ net.ipv4.igmp_max_memberships = 512
net.core.rps_sock_flow_entries = 32768
# Congestion control
-net.core.default_qdisc=fq
+net.core.default_qdisc=fq_codel
net.ipv4.tcp_congestion_control=bbr
diff --git a/src/helpers/config_dependency.py b/src/helpers/config_dependency.py
index 50c72956e..817bcc65a 100755
--- a/src/helpers/config_dependency.py
+++ b/src/helpers/config_dependency.py
@@ -18,22 +18,75 @@
import os
import sys
+import json
from argparse import ArgumentParser
from argparse import ArgumentTypeError
-
-try:
- from vyos.configdep import check_dependency_graph
- from vyos.defaults import directories
-except ImportError:
- # allow running during addon package build
- _here = os.path.dirname(__file__)
- sys.path.append(os.path.join(_here, '../../python/vyos'))
- from configdep import check_dependency_graph
- from defaults import directories
+from graphlib import TopologicalSorter, CycleError
# addon packages will need to specify the dependency directory
-dependency_dir = os.path.join(directories['data'],
- 'config-mode-dependencies')
+data_dir = '/usr/share/vyos/'
+dependency_dir = os.path.join(data_dir, 'config-mode-dependencies')
+
+def dict_merge(source, destination):
+ from copy import deepcopy
+ tmp = deepcopy(destination)
+
+ for key, value in source.items():
+ if key not in tmp:
+ tmp[key] = value
+ elif isinstance(source[key], dict):
+ tmp[key] = dict_merge(source[key], tmp[key])
+
+ return tmp
+
+def read_dependency_dict(dependency_dir: str = dependency_dir) -> dict:
+ res = {}
+ for dep_file in os.listdir(dependency_dir):
+ if not dep_file.endswith('.json'):
+ continue
+ path = os.path.join(dependency_dir, dep_file)
+ with open(path) as f:
+ d = json.load(f)
+ if dep_file == 'vyos-1x.json':
+ res = dict_merge(res, d)
+ else:
+ res = dict_merge(d, res)
+
+ return res
+
+def graph_from_dependency_dict(d: dict) -> dict:
+ g = {}
+ for k in list(d):
+ g[k] = set()
+ # add the dependencies for every sub-case; should there be cases
+ # that are mutally exclusive in the future, the graphs will be
+ # distinguished
+ for el in list(d[k]):
+ g[k] |= set(d[k][el])
+
+ return g
+
+def is_acyclic(d: dict) -> bool:
+ g = graph_from_dependency_dict(d)
+ ts = TopologicalSorter(g)
+ try:
+ # get node iterator
+ order = ts.static_order()
+ # try iteration
+ _ = [*order]
+ except CycleError:
+ return False
+
+ return True
+
+def check_dependency_graph(dependency_dir: str = dependency_dir,
+ supplement: str = None) -> bool:
+ d = read_dependency_dict(dependency_dir=dependency_dir)
+ if supplement is not None:
+ with open(supplement) as f:
+ d = dict_merge(json.load(f), d)
+
+ return is_acyclic(d)
def path_exists(s):
if not os.path.exists(s):
@@ -50,8 +103,10 @@ def main():
args = vars(parser.parse_args())
if not check_dependency_graph(**args):
+ print("dependency error: cycle exists")
sys.exit(1)
+ print("dependency graph acyclic")
sys.exit(0)
if __name__ == '__main__':
diff --git a/src/helpers/vyos-save-config.py b/src/helpers/vyos-save-config.py
index 8af4a7916..518bd9864 100755
--- a/src/helpers/vyos-save-config.py
+++ b/src/helpers/vyos-save-config.py
@@ -19,6 +19,7 @@ import os
import re
import sys
from tempfile import NamedTemporaryFile
+from argparse import ArgumentParser
from vyos.config import Config
from vyos.remote import urlc
@@ -28,8 +29,15 @@ from vyos.defaults import directories
DEFAULT_CONFIG_PATH = os.path.join(directories['config'], 'config.boot')
remote_save = None
-if len(sys.argv) > 1:
- save_file = sys.argv[1]
+parser = ArgumentParser(description='Save configuration')
+parser.add_argument('file', type=str, nargs='?', help='Save configuration to file')
+parser.add_argument('--write-json-file', type=str, help='Save JSON of configuration to file')
+args = parser.parse_args()
+file = args.file
+json_file = args.write_json_file
+
+if file is not None:
+ save_file = file
else:
save_file = DEFAULT_CONFIG_PATH
@@ -51,6 +59,13 @@ with open(write_file, 'w') as f:
f.write("\n")
f.write(system_footer())
+if json_file is not None and ct is not None:
+ try:
+ with open(json_file, 'w') as f:
+ f.write(ct.to_json())
+ except OSError as e:
+ print(f'failed to write JSON file: {e}')
+
if remote_save is not None:
try:
remote_save.upload(write_file)
diff --git a/src/init/vyos-router b/src/init/vyos-router
index cf97d4c6e..35095afe4 100755
--- a/src/init/vyos-router
+++ b/src/init/vyos-router
@@ -234,10 +234,31 @@ cleanup_post_commit_hooks () {
# system defaults.
security_reset ()
{
+
+ # restore NSS cofniguration back to sane system defaults
+ # will be overwritten later when configuration is loaded
+ cat <<EOF >/etc/nsswitch.conf
+passwd: files
+group: files
+shadow: files
+gshadow: files
+
+# Per T2678, commenting out myhostname
+hosts: files dns #myhostname
+networks: files
+
+protocols: db files
+services: db files
+ethers: db files
+rpc: db files
+
+netgroup: nis
+EOF
+
# restore PAM back to virgin state (no radius/tacacs services)
- pam-auth-update --package --remove radius
+ pam-auth-update --disable radius-mandatory radius-optional
rm -f /etc/pam_radius_auth.conf
- pam-auth-update --package --remove tacplus
+ pam-auth-update --disable tacplus-mandatory tacplus-optional
rm -f /etc/tacplus_nss.conf /etc/tacplus_servers
# Certain configuration files are re-generated by the configuration
@@ -349,7 +370,6 @@ start ()
# As VyOS does not execute commands that are not present in the CLI we call
# the script by hand to have a single source for the login banner and MOTD
${vyos_conf_scripts_dir}/system_console.py || log_failure_msg "could not reset serial console"
- ${vyos_conf_scripts_dir}/system-login.py || log_failure_msg "could not reset system login"
${vyos_conf_scripts_dir}/system-login-banner.py || log_failure_msg "could not reset motd and issue files"
${vyos_conf_scripts_dir}/system-option.py || log_failure_msg "could not reset system option files"
${vyos_conf_scripts_dir}/system-ip.py || log_failure_msg "could not reset system IPv4 options"
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index 66e80ced5..3a9efb73e 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -22,12 +22,14 @@ import grp
import copy
import json
import logging
+import signal
import traceback
import threading
+from time import sleep
from typing import List, Union, Callable, Dict
-import uvicorn
from fastapi import FastAPI, Depends, Request, Response, HTTPException
+from fastapi import BackgroundTasks
from fastapi.responses import HTMLResponse
from fastapi.exceptions import RequestValidationError
from fastapi.routing import APIRoute
@@ -36,10 +38,14 @@ from starlette.middleware.cors import CORSMiddleware
from starlette.datastructures import FormData
from starlette.formparsers import FormParser, MultiPartParser
from multipart.multipart import parse_options_header
+from uvicorn import Config as UvicornConfig
+from uvicorn import Server as UvicornServer
from ariadne.asgi import GraphQL
-import vyos.config
+from vyos.config import Config
+from vyos.configtree import ConfigTree
+from vyos.configdiff import get_config_diff
from vyos.configsession import ConfigSession, ConfigSessionError
import api.graphql.state
@@ -410,12 +416,24 @@ app.router.route_class = MultipartRoute
async def validation_exception_handler(request, exc):
return error(400, str(exc.errors()[0]))
+self_ref_msg = "Requested HTTP API server configuration change; commit will be called in the background"
+
+def call_commit(s: ConfigSession):
+ try:
+ s.commit()
+ except ConfigSessionError as e:
+ s.discard()
+ if app.state.vyos_debug:
+ logger.warning(f"ConfigSessionError:\n {traceback.format_exc()}")
+ else:
+ logger.warning(f"ConfigSessionError: {e}")
+
def _configure_op(data: Union[ConfigureModel, ConfigureListModel,
ConfigSectionModel, ConfigSectionListModel],
- request: Request):
+ request: Request, background_tasks: BackgroundTasks):
session = app.state.vyos_session
env = session.get_session_env()
- config = vyos.config.Config(session_env=env)
+ config = Config(session_env=env)
endpoint = request.url.path
@@ -470,7 +488,15 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,
else:
raise ConfigSessionError(f"'{op}' is not a valid operation")
# end for
- session.commit()
+ config = Config(session_env=env)
+ d = get_config_diff(config)
+
+ if d.is_node_changed(['service', 'https']):
+ background_tasks.add_task(call_commit, session)
+ msg = self_ref_msg
+ else:
+ session.commit()
+
logger.info(f"Configuration modified via HTTP API using key '{app.state.vyos_id}'")
except ConfigSessionError as e:
session.discard()
@@ -495,21 +521,21 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,
@app.post('/configure')
def configure_op(data: Union[ConfigureModel,
- ConfigureListModel],
- request: Request):
- return _configure_op(data, request)
+ ConfigureListModel],
+ request: Request, background_tasks: BackgroundTasks):
+ return _configure_op(data, request, background_tasks)
@app.post('/configure-section')
def configure_section_op(data: Union[ConfigSectionModel,
- ConfigSectionListModel],
- request: Request):
- return _configure_op(data, request)
+ ConfigSectionListModel],
+ request: Request, background_tasks: BackgroundTasks):
+ return _configure_op(data, request, background_tasks)
@app.post("/retrieve")
async def retrieve_op(data: RetrieveModel):
session = app.state.vyos_session
env = session.get_session_env()
- config = vyos.config.Config(session_env=env)
+ config = Config(session_env=env)
op = data.op
path = " ".join(data.path)
@@ -528,10 +554,10 @@ async def retrieve_op(data: RetrieveModel):
res = session.show_config(path=data.path)
if config_format == 'json':
- config_tree = vyos.configtree.ConfigTree(res)
+ config_tree = ConfigTree(res)
res = json.loads(config_tree.to_json())
elif config_format == 'json_ast':
- config_tree = vyos.configtree.ConfigTree(res)
+ config_tree = ConfigTree(res)
res = json.loads(config_tree.to_json_ast())
elif config_format == 'raw':
pass
@@ -548,10 +574,11 @@ async def retrieve_op(data: RetrieveModel):
return success(res)
@app.post('/config-file')
-def config_file_op(data: ConfigFileModel):
+def config_file_op(data: ConfigFileModel, background_tasks: BackgroundTasks):
session = app.state.vyos_session
-
+ env = session.get_session_env()
op = data.op
+ msg = None
try:
if op == 'save':
@@ -559,14 +586,23 @@ def config_file_op(data: ConfigFileModel):
path = data.file
else:
path = '/config/config.boot'
- res = session.save_config(path)
+ msg = session.save_config(path)
elif op == 'load':
if data.file:
path = data.file
else:
return error(400, "Missing required field \"file\"")
- res = session.migrate_and_load_config(path)
- res = session.commit()
+
+ session.migrate_and_load_config(path)
+
+ config = Config(session_env=env)
+ d = get_config_diff(config)
+
+ if d.is_node_changed(['service', 'https']):
+ background_tasks.add_task(call_commit, session)
+ msg = self_ref_msg
+ else:
+ session.commit()
else:
return error(400, f"'{op}' is not a valid operation")
except ConfigSessionError as e:
@@ -575,7 +611,7 @@ def config_file_op(data: ConfigFileModel):
logger.critical(traceback.format_exc())
return error(500, "An internal error occured. Check the logs for details.")
- return success(res)
+ return success(msg)
@app.post('/image')
def image_op(data: ImageModel):
@@ -607,7 +643,7 @@ def image_op(data: ImageModel):
return success(res)
@app.post('/container-image')
-def image_op(data: ContainerImageModel):
+def container_image_op(data: ContainerImageModel):
session = app.state.vyos_session
op = data.op
@@ -702,7 +738,7 @@ def reset_op(data: ResetModel):
# GraphQL integration
###
-def graphql_init(fast_api_app):
+def graphql_init(app: FastAPI = app):
from api.graphql.libs.token_auth import get_user_context
api.graphql.state.init()
api.graphql.state.settings['app'] = app
@@ -728,26 +764,45 @@ def graphql_init(fast_api_app):
debug=True,
introspection=in_spec))
###
+# Modify uvicorn to allow reloading server within the configsession
+###
-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)
+server = None
+shutdown = False
- # Need to set file permissions to 775 too so that every vyattacfg group member
- # has write access to the running config
- os.umask(0o002)
+class ApiServerConfig(UvicornConfig):
+ pass
+
+class ApiServer(UvicornServer):
+ def install_signal_handlers(self):
+ pass
+
+def reload_handler(signum, frame):
+ global server
+ logger.debug('Reload signal received...')
+ if server is not None:
+ server.handle_exit(signum, frame)
+ server = None
+ logger.info('Server stopping for reload...')
+ else:
+ logger.warning('Reload called for non-running server...')
+def shutdown_handler(signum, frame):
+ global shutdown
+ logger.debug('Shutdown signal received...')
+ server.handle_exit(signum, frame)
+ logger.info('Server shutdown...')
+ shutdown = True
+
+def initialization(session: ConfigSession, app: FastAPI = app):
+ global server
try:
server_config = load_server_config()
- except Exception as err:
- logger.critical(f"Failed to load the HTTP API server config: {err}")
+ except Exception as e:
+ logger.critical(f'Failed to load the HTTP API server config: {e}')
sys.exit(1)
- config_session = ConfigSession(os.getpid())
-
- app.state.vyos_session = config_session
+ app.state.vyos_session = session
app.state.vyos_keys = server_config['api_keys']
app.state.vyos_debug = server_config['debug']
@@ -770,14 +825,44 @@ if __name__ == '__main__':
if app.state.vyos_graphql:
graphql_init(app)
+ if not server_config['socket']:
+ config = ApiServerConfig(app,
+ host=server_config["listen_address"],
+ port=int(server_config["port"]),
+ proxy_headers=True)
+ else:
+ config = ApiServerConfig(app,
+ uds="/run/api.sock",
+ proxy_headers=True)
+ server = ApiServer(config)
+
+def run_server():
try:
- if not server_config['socket']:
- uvicorn.run(app, host=server_config["listen_address"],
- port=int(server_config["port"]),
- proxy_headers=True)
- else:
- uvicorn.run(app, uds="/run/api.sock",
- proxy_headers=True)
- except OSError as err:
- logger.critical(f"OSError {err}")
+ server.run()
+ except OSError as e:
+ logger.critical(e)
sys.exit(1)
+
+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)
+
+ signal.signal(signal.SIGHUP, reload_handler)
+ signal.signal(signal.SIGTERM, shutdown_handler)
+
+ config_session = ConfigSession(os.getpid())
+
+ while True:
+ logger.debug('Enter main loop...')
+ if shutdown:
+ break
+ if server is None:
+ initialization(config_session)
+ server.run()
+ sleep(1)