From f9c81b121b146fbcf3c458273b2b47e6e571f2e2 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 2 Oct 2024 12:20:15 -0500 Subject: config-mgmt: T5976: normalize formatting --- python/vyos/config_mgmt.py | 271 +++++++++++++++++------------- python/vyos/configsession.py | 78 +++++++-- src/conf_mode/system_config-management.py | 13 +- src/helpers/commit-confirm-notify.py | 18 +- 4 files changed, 236 insertions(+), 144 deletions(-) diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py index d518737ca..920a19fec 100644 --- a/python/vyos/config_mgmt.py +++ b/python/vyos/config_mgmt.py @@ -49,8 +49,10 @@ config_json = '/run/vyatta/config/config.json' # created by vyatta-cfg-postinst commit_post_hook_dir = '/etc/commit/post-hooks.d' -commit_hooks = {'commit_revision': '01vyos-commit-revision', - 'commit_archive': '02vyos-commit-archive'} +commit_hooks = { + 'commit_revision': '01vyos-commit-revision', + 'commit_archive': '02vyos-commit-archive', +} DEFAULT_TIME_MINUTES = 10 timer_name = 'commit-confirm' @@ -72,6 +74,7 @@ formatter = logging.Formatter('%(funcName)s: %(levelname)s:%(message)s') ch.setFormatter(formatter) logger.addHandler(ch) + def save_config(target, json_out=None): if json_out is None: cmd = f'{SAVE_CONFIG} {target}' @@ -81,6 +84,7 @@ def save_config(target, json_out=None): if rc != 0: logger.critical(f'save config failed: {out}') + def unsaved_commits(allow_missing_config=False) -> bool: if get_full_version_data()['boot_via'] == 'livecd': return False @@ -92,6 +96,7 @@ def unsaved_commits(allow_missing_config=False) -> bool: os.unlink(tmp_save) return ret + def get_file_revision(rev: int): revision = os.path.join(archive_dir, f'config.boot.{rev}.gz') try: @@ -102,12 +107,15 @@ def get_file_revision(rev: int): return '' return r + def get_config_tree_revision(rev: int): c = get_file_revision(rev) return ConfigTree(c) + def is_node_revised(path: list = [], rev1: int = 1, rev2: int = 0) -> bool: from vyos.configtree import DiffTree + left = get_config_tree_revision(rev1) right = get_config_tree_revision(rev2) diff_tree = DiffTree(left, right) @@ -115,9 +123,11 @@ def is_node_revised(path: list = [], rev1: int = 1, rev2: int = 0) -> bool: return True return False + class ConfigMgmtError(Exception): pass + class ConfigMgmt: def __init__(self, session_env=None, config=None): if session_env: @@ -128,15 +138,14 @@ class ConfigMgmt: if config is None: config = Config() - d = config.get_config_dict(['system', 'config-management'], - key_mangling=('-', '_'), - get_first_key=True) + d = config.get_config_dict( + ['system', 'config-management'], key_mangling=('-', '_'), get_first_key=True + ) self.max_revisions = int(d.get('commit_revisions', 0)) self.num_revisions = 0 self.locations = d.get('commit_archive', {}).get('location', []) - self.source_address = d.get('commit_archive', - {}).get('source_address', '') + self.source_address = d.get('commit_archive', {}).get('source_address', '') if config.exists(['system', 'host-name']): self.hostname = config.return_value(['system', 'host-name']) if config.exists(['system', 'domain-name']): @@ -156,15 +165,16 @@ class ConfigMgmt: # a call to compare without args is edit_level aware edit_level = os.getenv('VYATTA_EDIT_LEVEL', '') - self.edit_path = [l for l in edit_level.split('/') if l] + self.edit_path = [l for l in edit_level.split('/') if l] # noqa: E741 self.active_config = config._running_config self.working_config = config._session_config # Console script functions # - def commit_confirm(self, minutes: int=DEFAULT_TIME_MINUTES, - no_prompt: bool=False) -> Tuple[str,int]: + def commit_confirm( + self, minutes: int = DEFAULT_TIME_MINUTES, no_prompt: bool = False + ) -> Tuple[str, int]: """Commit with reboot to saved config in 'minutes' minutes if 'confirm' call is not issued. """ @@ -177,10 +187,10 @@ class ConfigMgmt: else: W = '' - prompt_str = f''' + prompt_str = f""" commit-confirm will automatically reboot in {minutes} minutes unless changes are confirmed.\n -Proceed ?''' +Proceed ?""" prompt_str = W + prompt_str if not no_prompt and not ask_yes_no(prompt_str, default=True): msg = 'commit-confirm canceled' @@ -199,7 +209,7 @@ Proceed ?''' msg = f'Initialized commit-confirm; {minutes} minutes to confirm before reboot' return msg, 0 - def confirm(self) -> Tuple[str,int]: + def confirm(self) -> Tuple[str, int]: """Do not reboot to saved config following 'commit-confirm'. Update commit log and archive. """ @@ -227,9 +237,8 @@ Proceed ?''' msg = 'Reboot timer stopped' return msg, 0 - def revert(self) -> Tuple[str,int]: - """Reboot to saved config, dropping commits from 'commit-confirm'. - """ + def revert(self) -> Tuple[str, int]: + """Reboot to saved config, dropping commits from 'commit-confirm'.""" _ = self._read_tmp_log_entry() # archived config will be reverted on boot @@ -239,13 +248,14 @@ Proceed ?''' return '', 0 - def rollback(self, rev: int, no_prompt: bool=False) -> Tuple[str,int]: - """Reboot to config revision 'rev'. - """ + def rollback(self, rev: int, no_prompt: bool = False) -> Tuple[str, int]: + """Reboot to config revision 'rev'.""" msg = '' if not self._check_revision_number(rev): - msg = f'Invalid revision number {rev}: must be 0 < rev < {self.num_revisions}' + msg = ( + f'Invalid revision number {rev}: must be 0 < rev < {self.num_revisions}' + ) return msg, 1 prompt_str = 'Proceed with reboot ?' @@ -274,12 +284,13 @@ Proceed ?''' return msg, 0 def rollback_soft(self, rev: int): - """Rollback without reboot (rollback-soft) - """ + """Rollback without reboot (rollback-soft)""" msg = '' if not self._check_revision_number(rev): - msg = f'Invalid revision number {rev}: must be 0 < rev < {self.num_revisions}' + msg = ( + f'Invalid revision number {rev}: must be 0 < rev < {self.num_revisions}' + ) return msg, 1 rollback_ct = self._get_config_tree_revision(rev) @@ -292,9 +303,13 @@ Proceed ?''' return msg, 0 - def compare(self, saved: bool=False, commands: bool=False, - rev1: Optional[int]=None, - rev2: Optional[int]=None) -> Tuple[str,int]: + def compare( + self, + saved: bool = False, + commands: bool = False, + rev1: Optional[int] = None, + rev2: Optional[int] = None, + ) -> Tuple[str, int]: """General compare function for config file revisions: revision n vs. revision m; working version vs. active version; or working version vs. saved version. @@ -335,7 +350,7 @@ Proceed ?''' return msg, 0 - def wrap_compare(self, options) -> Tuple[str,int]: + def wrap_compare(self, options) -> Tuple[str, int]: """Interface to vyatta-cfg-run: args collected as 'options' to parse for compare. """ @@ -343,7 +358,7 @@ Proceed ?''' r1 = None r2 = None if 'commands' in options: - cmnds=True + cmnds = True options.remove('commands') for i in options: if not i.isnumeric(): @@ -358,8 +373,7 @@ Proceed ?''' # Initialization and post-commit hooks for conf-mode # def initialize_revision(self): - """Initialize config archive, logrotate conf, and commit log. - """ + """Initialize config archive, logrotate conf, and commit log.""" mask = os.umask(0o002) os.makedirs(archive_dir, exist_ok=True) json_dir = os.path.dirname(config_json) @@ -371,8 +385,7 @@ Proceed ?''' self._add_logrotate_conf() - if (not os.path.exists(commit_log_file) or - self._get_number_of_revisions() == 0): + if not os.path.exists(commit_log_file) or self._get_number_of_revisions() == 0: user = self._get_user() via = 'init' comment = '' @@ -399,8 +412,7 @@ Proceed ?''' self._update_archive() def commit_archive(self): - """Upload config to remote archive. - """ + """Upload config to remote archive.""" from vyos.remote import upload hostname = self.hostname @@ -410,20 +422,23 @@ Proceed ?''' source_address = self.source_address if self.effective_locations: - print("Archiving config...") + print('Archiving config...') for location in self.effective_locations: url = urlsplit(location) - _, _, netloc = url.netloc.rpartition("@") + _, _, netloc = url.netloc.rpartition('@') redacted_location = urlunsplit(url._replace(netloc=netloc)) - print(f" {redacted_location}", end=" ", flush=True) - upload(archive_config_file, f'{location}/{remote_file}', - source_host=source_address) + print(f' {redacted_location}', end=' ', flush=True) + upload( + archive_config_file, + f'{location}/{remote_file}', + source_host=source_address, + ) # op-mode functions # def get_raw_log_data(self) -> list: """Return list of dicts of log data: - keys: [timestamp, user, commit_via, commit_comment] + keys: [timestamp, user, commit_via, commit_comment] """ log = self._get_log_entries() res_l = [] @@ -435,20 +450,20 @@ Proceed ?''' @staticmethod def format_log_data(data: list) -> str: - """Return formatted log data as str. - """ + """Return formatted log data as str.""" res_l = [] - for l_no, l in enumerate(data): - time_d = datetime.fromtimestamp(int(l['timestamp'])) - time_str = time_d.strftime("%Y-%m-%d %H:%M:%S") + for l_no, l_val in enumerate(data): + time_d = datetime.fromtimestamp(int(l_val['timestamp'])) + time_str = time_d.strftime('%Y-%m-%d %H:%M:%S') - res_l.append([l_no, time_str, - f"by {l['user']}", f"via {l['commit_via']}"]) + res_l.append( + [l_no, time_str, f"by {l_val['user']}", f"via {l_val['commit_via']}"] + ) - if l['commit_comment'] != 'commit': # default comment - res_l.append([None, l['commit_comment']]) + if l_val['commit_comment'] != 'commit': # default comment + res_l.append([None, l_val['commit_comment']]) - ret = tabulate(res_l, tablefmt="plain") + ret = tabulate(res_l, tablefmt='plain') return ret @staticmethod @@ -459,23 +474,25 @@ Proceed ?''' 'rollback'. """ res_l = [] - for l_no, l in enumerate(data): - time_d = datetime.fromtimestamp(int(l['timestamp'])) - time_str = time_d.strftime("%Y-%m-%d %H:%M:%S") + for l_no, l_val in enumerate(data): + time_d = datetime.fromtimestamp(int(l_val['timestamp'])) + time_str = time_d.strftime('%Y-%m-%d %H:%M:%S') - res_l.append(['\t', l_no, time_str, - f"{l['user']}", f"by {l['commit_via']}"]) + res_l.append( + ['\t', l_no, time_str, f"{l_val['user']}", f"by {l_val['commit_via']}"] + ) - ret = tabulate(res_l, tablefmt="plain") + ret = tabulate(res_l, tablefmt='plain') return ret - def show_commit_diff(self, rev: int, rev2: Optional[int]=None, - commands: bool=False) -> str: + def show_commit_diff( + self, rev: int, rev2: Optional[int] = None, commands: bool = False + ) -> str: """Show commit diff at revision number, compared to previous revision, or to another revision. """ if rev2 is None: - out, _ = self.compare(commands=commands, rev1=rev, rev2=(rev+1)) + out, _ = self.compare(commands=commands, rev1=rev, rev2=(rev + 1)) return out out, _ = self.compare(commands=commands, rev1=rev, rev2=rev2) @@ -519,8 +536,9 @@ Proceed ?''' conf_file.chmod(0o644) def _archive_active_config(self) -> bool: - save_to_tmp = (boot_configuration_complete() or not - os.path.isfile(archive_config_file)) + save_to_tmp = boot_configuration_complete() or not os.path.isfile( + archive_config_file + ) mask = os.umask(0o113) ext = os.getpid() @@ -560,15 +578,14 @@ Proceed ?''' @staticmethod def _update_archive(): - cmd = f"sudo logrotate -f -s {logrotate_state} {logrotate_conf}" + cmd = f'sudo logrotate -f -s {logrotate_state} {logrotate_conf}' rc, out = rc_cmd(cmd) if rc != 0: logger.critical(f'logrotate failure: {out}') @staticmethod def _get_log_entries() -> list: - """Return lines of commit log as list of strings - """ + """Return lines of commit log as list of strings""" entries = [] if os.path.exists(commit_log_file): with open(commit_log_file) as f: @@ -577,8 +594,8 @@ Proceed ?''' return entries def _get_number_of_revisions(self) -> int: - l = self._get_log_entries() - return len(l) + log_entries = self._get_log_entries() + return len(log_entries) def _check_revision_number(self, rev: int) -> bool: self.num_revisions = self._get_number_of_revisions() @@ -599,9 +616,14 @@ Proceed ?''' user = 'unknown' return user - def _new_log_entry(self, user: str='', commit_via: str='', - commit_comment: str='', timestamp: Optional[int]=None, - tmp_file: str=None) -> Optional[str]: + def _new_log_entry( + self, + user: str = '', + commit_via: str = '', + commit_comment: str = '', + timestamp: Optional[int] = None, + tmp_file: str = None, + ) -> Optional[str]: # Format log entry and return str or write to file. # # Usage is within a post-commit hook, using env values. In case of @@ -647,12 +669,12 @@ Proceed ?''' logger.critical(f'Invalid log format {line}') return {} - timestamp, user, commit_via, commit_comment = ( - tuple(line.strip().strip('|').split('|'))) + timestamp, user, commit_via, commit_comment = tuple( + line.strip().strip('|').split('|') + ) commit_comment = commit_comment.replace('%%', '|') - d = dict(zip(keys, [user, commit_via, - commit_comment, timestamp])) + d = dict(zip(keys, [user, commit_via, commit_comment, timestamp])) return d @@ -666,13 +688,21 @@ Proceed ?''' return self._get_log_entry(entry) - def _add_log_entry(self, user: str='', commit_via: str='', - commit_comment: str='', timestamp: Optional[int]=None): + def _add_log_entry( + self, + user: str = '', + commit_via: str = '', + commit_comment: str = '', + timestamp: Optional[int] = None, + ): mask = os.umask(0o113) - entry = self._new_log_entry(user=user, commit_via=commit_via, - commit_comment=commit_comment, - timestamp=timestamp) + entry = self._new_log_entry( + user=user, + commit_via=commit_via, + commit_comment=commit_comment, + timestamp=timestamp, + ) log_entries = self._get_log_entries() log_entries.insert(0, entry) @@ -687,6 +717,7 @@ Proceed ?''' os.umask(mask) + # entry_point for console script # def run(): @@ -706,43 +737,53 @@ def run(): parser = ArgumentParser() subparsers = parser.add_subparsers(dest='subcommand') - commit_confirm = subparsers.add_parser('commit_confirm', - help="Commit with opt-out reboot to saved config") - commit_confirm.add_argument('-t', dest='minutes', type=int, - default=DEFAULT_TIME_MINUTES, - help="Minutes until reboot, unless 'confirm'") - commit_confirm.add_argument('-y', dest='no_prompt', action='store_true', - help="Execute without prompt") - - subparsers.add_parser('confirm', help="Confirm commit") - subparsers.add_parser('revert', help="Revert commit-confirm") - - rollback = subparsers.add_parser('rollback', - help="Rollback to earlier config") - rollback.add_argument('--rev', type=int, - help="Revision number for rollback") - rollback.add_argument('-y', dest='no_prompt', action='store_true', - help="Excute without prompt") - - rollback_soft = subparsers.add_parser('rollback_soft', - help="Rollback to earlier config") - rollback_soft.add_argument('--rev', type=int, - help="Revision number for rollback") - - compare = subparsers.add_parser('compare', - help="Compare config files") - - compare.add_argument('--saved', action='store_true', - help="Compare session config with saved config") - compare.add_argument('--commands', action='store_true', - help="Show difference between commands") - compare.add_argument('--rev1', type=int, default=None, - help="Compare revision with session config or other revision") - compare.add_argument('--rev2', type=int, default=None, - help="Compare revisions") - - wrap_compare = subparsers.add_parser('wrap_compare', - help="Wrapper interface for vyatta-cfg-run") + commit_confirm = subparsers.add_parser( + 'commit_confirm', help='Commit with opt-out reboot to saved config' + ) + commit_confirm.add_argument( + '-t', + dest='minutes', + type=int, + default=DEFAULT_TIME_MINUTES, + help="Minutes until reboot, unless 'confirm'", + ) + commit_confirm.add_argument( + '-y', dest='no_prompt', action='store_true', help='Execute without prompt' + ) + + subparsers.add_parser('confirm', help='Confirm commit') + subparsers.add_parser('revert', help='Revert commit-confirm') + + rollback = subparsers.add_parser('rollback', help='Rollback to earlier config') + rollback.add_argument('--rev', type=int, help='Revision number for rollback') + rollback.add_argument( + '-y', dest='no_prompt', action='store_true', help='Excute without prompt' + ) + + rollback_soft = subparsers.add_parser( + 'rollback_soft', help='Rollback to earlier config' + ) + rollback_soft.add_argument('--rev', type=int, help='Revision number for rollback') + + compare = subparsers.add_parser('compare', help='Compare config files') + + compare.add_argument( + '--saved', action='store_true', help='Compare session config with saved config' + ) + compare.add_argument( + '--commands', action='store_true', help='Show difference between commands' + ) + compare.add_argument( + '--rev1', + type=int, + default=None, + help='Compare revision with session config or other revision', + ) + compare.add_argument('--rev2', type=int, default=None, help='Compare revisions') + + wrap_compare = subparsers.add_parser( + 'wrap_compare', help='Wrapper interface for vyatta-cfg-run' + ) wrap_compare.add_argument('--options', nargs=REMAINDER) args = vars(parser.parse_args()) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 7d51b94e4..c0d3c7ecb 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -32,15 +32,34 @@ SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig'] LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile'] MIGRATE_LOAD_CONFIG = ['/usr/libexec/vyos/vyos-load-config.py'] SAVE_CONFIG = ['/usr/libexec/vyos/vyos-save-config.py'] -INSTALL_IMAGE = ['/usr/libexec/vyos/op_mode/image_installer.py', - '--action', 'add', '--no-prompt', '--image-path'] +INSTALL_IMAGE = [ + '/usr/libexec/vyos/op_mode/image_installer.py', + '--action', + 'add', + '--no-prompt', + '--image-path', +] IMPORT_PKI = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'import'] -IMPORT_PKI_NO_PROMPT = ['/usr/libexec/vyos/op_mode/pki.py', - '--action', 'import', '--no-prompt'] -REMOVE_IMAGE = ['/usr/libexec/vyos/op_mode/image_manager.py', - '--action', 'delete', '--no-prompt', '--image-name'] -SET_DEFAULT_IMAGE = ['/usr/libexec/vyos/op_mode/image_manager.py', - '--action', 'set', '--no-prompt', '--image-name'] +IMPORT_PKI_NO_PROMPT = [ + '/usr/libexec/vyos/op_mode/pki.py', + '--action', + 'import', + '--no-prompt', +] +REMOVE_IMAGE = [ + '/usr/libexec/vyos/op_mode/image_manager.py', + '--action', + 'delete', + '--no-prompt', + '--image-name', +] +SET_DEFAULT_IMAGE = [ + '/usr/libexec/vyos/op_mode/image_manager.py', + '--action', + 'set', + '--no-prompt', + '--image-name', +] GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate'] SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show'] RESET = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'reset'] @@ -50,7 +69,8 @@ OP_CMD_ADD = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'add'] OP_CMD_DELETE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'delete'] # Default "commit via" string -APP = "vyos-http-api" +APP = 'vyos-http-api' + # When started as a service rather than from a user shell, # the process lacks the VyOS-specific environment that comes @@ -61,7 +81,7 @@ def inject_vyos_env(env): env['VYATTA_USER_LEVEL_DIR'] = '/opt/vyatta/etc/shell/level/admin' env['VYATTA_PROCESS_CLIENT'] = 'gui2_rest' env['VYOS_HEADLESS_CLIENT'] = 'vyos_http_api' - env['vyatta_bindir']= '/opt/vyatta/bin' + env['vyatta_bindir'] = '/opt/vyatta/bin' env['vyatta_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates' env['vyatta_configdir'] = directories['vyos_configdir'] env['vyatta_datadir'] = '/opt/vyatta/share' @@ -78,7 +98,7 @@ def inject_vyos_env(env): env['vyos_configdir'] = directories['vyos_configdir'] env['vyos_conf_scripts_dir'] = '/usr/libexec/vyos/conf_mode' env['vyos_datadir'] = '/opt/vyatta/share' - env['vyos_datarootdir']= '/opt/vyatta/share' + env['vyos_datarootdir'] = '/opt/vyatta/share' env['vyos_libdir'] = '/opt/vyatta/lib' env['vyos_libexec_dir'] = '/usr/libexec/vyos' env['vyos_op_scripts_dir'] = '/usr/libexec/vyos/op_mode' @@ -102,6 +122,7 @@ class ConfigSession(object): """ The write API of VyOS. """ + def __init__(self, session_id, app=APP): """ Creates a new config session. @@ -116,7 +137,9 @@ class ConfigSession(object): and used the PID for the session identifier. """ - env_str = subprocess.check_output([CLI_SHELL_API, 'getSessionEnv', str(session_id)]) + env_str = subprocess.check_output( + [CLI_SHELL_API, 'getSessionEnv', str(session_id)] + ) self.__session_id = session_id # Extract actual variables from the chunk of shell it outputs @@ -129,20 +152,39 @@ class ConfigSession(object): session_env[k] = v self.__session_env = session_env - self.__session_env["COMMIT_VIA"] = app + self.__session_env['COMMIT_VIA'] = app self.__run_command([CLI_SHELL_API, 'setupSession']) def __del__(self): try: - output = subprocess.check_output([CLI_SHELL_API, 'teardownSession'], env=self.__session_env).decode().strip() + output = ( + subprocess.check_output( + [CLI_SHELL_API, 'teardownSession'], env=self.__session_env + ) + .decode() + .strip() + ) if output: - print("cli-shell-api teardownSession output for sesion {0}: {1}".format(self.__session_id, output), file=sys.stderr) + print( + 'cli-shell-api teardownSession output for sesion {0}: {1}'.format( + self.__session_id, output + ), + file=sys.stderr, + ) except Exception as e: - print("Could not tear down session {0}: {1}".format(self.__session_id, e), file=sys.stderr) + print( + 'Could not tear down session {0}: {1}'.format(self.__session_id, e), + file=sys.stderr, + ) def __run_command(self, cmd_list): - p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.__session_env) + p = subprocess.Popen( + cmd_list, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=self.__session_env, + ) (stdout_data, stderr_data) = p.communicate() output = stdout_data.decode() result = p.wait() @@ -204,7 +246,7 @@ class ConfigSession(object): def comment(self, path, value=None): if not value: - value = [""] + value = [''] else: value = [value] self.__run_command([COMMENT] + path + value) diff --git a/src/conf_mode/system_config-management.py b/src/conf_mode/system_config-management.py index c681a8405..99f25bef6 100755 --- a/src/conf_mode/system_config-management.py +++ b/src/conf_mode/system_config-management.py @@ -22,6 +22,7 @@ from vyos.config import Config from vyos.config_mgmt import ConfigMgmt from vyos.config_mgmt import commit_post_hook_dir, commit_hooks + def get_config(config=None): if config: conf = config @@ -36,22 +37,24 @@ def get_config(config=None): return mgmt + def verify(_mgmt): return + def generate(mgmt): if mgmt is None: return mgmt.initialize_revision() + def apply(mgmt): if mgmt is None: return locations = mgmt.locations - archive_target = os.path.join(commit_post_hook_dir, - commit_hooks['commit_archive']) + archive_target = os.path.join(commit_post_hook_dir, commit_hooks['commit_archive']) if locations: try: os.symlink('/usr/bin/config-mgmt', archive_target) @@ -68,8 +71,9 @@ def apply(mgmt): raise ConfigError from exc revisions = mgmt.max_revisions - revision_target = os.path.join(commit_post_hook_dir, - commit_hooks['commit_revision']) + revision_target = os.path.join( + commit_post_hook_dir, commit_hooks['commit_revision'] + ) if revisions > 0: try: os.symlink('/usr/bin/config-mgmt', revision_target) @@ -85,6 +89,7 @@ def apply(mgmt): except OSError as exc: raise ConfigError from exc + if __name__ == '__main__': try: c = get_config() diff --git a/src/helpers/commit-confirm-notify.py b/src/helpers/commit-confirm-notify.py index 8d7626c78..69dda5112 100755 --- a/src/helpers/commit-confirm-notify.py +++ b/src/helpers/commit-confirm-notify.py @@ -6,15 +6,19 @@ import time # Minutes before reboot to trigger notification. intervals = [1, 5, 15, 60] + def notify(interval): - s = "" if interval == 1 else "s" + s = '' if interval == 1 else 's' time.sleep((minutes - interval) * 60) - message = ('"[commit-confirm] System is going to reboot in ' - f'{interval} minute{s} to rollback the last commit.\n' - 'Confirm your changes to cancel the reboot."') - os.system("wall -n " + message) + message = ( + '"[commit-confirm] System is going to reboot in ' + f'{interval} minute{s} to rollback the last commit.\n' + 'Confirm your changes to cancel the reboot."' + ) + os.system('wall -n ' + message) + -if __name__ == "__main__": +if __name__ == '__main__': # Must be run as root to call wall(1) without a banner. if len(sys.argv) != 2 or os.getuid() != 0: print('This script requires superuser privileges.', file=sys.stderr) @@ -27,5 +31,5 @@ if __name__ == "__main__": for interval in sorted(intervals, reverse=True): if minutes >= interval: notify(interval) - minutes -= (minutes - interval) + minutes -= minutes - interval exit(0) -- cgit v1.2.3 From 4d5f2a58bbf5a366c43871ef27b75d31b3b2a114 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 2 Oct 2024 12:36:41 -0500 Subject: config-mgmt: T5976: add option for commit-confirm to use 'soft' rollback Commit-confirm will restore a previous configuration if a confirmation is not received in N minutes. Traditionally, this was restored by a reboot into the last configuration on disk; add a configurable option to reload the last completed commit without a reboot. The default setting is to reboot. --- .../system_config-management.xml.in | 20 ++++++ python/vyos/config_mgmt.py | 83 ++++++++++++++++++---- python/vyos/configsession.py | 9 +++ src/conf_mode/system_config-management.py | 6 +- src/helpers/commit-confirm-notify.py | 42 ++++++++--- 5 files changed, 137 insertions(+), 23 deletions(-) diff --git a/interface-definitions/system_config-management.xml.in b/interface-definitions/system_config-management.xml.in index e666633b7..b8fb6cdb5 100644 --- a/interface-definitions/system_config-management.xml.in +++ b/interface-definitions/system_config-management.xml.in @@ -67,6 +67,26 @@ Number of revisions must be between 0 and 65535 + + + Commit confirm rollback type if no confirmation + + reload reboot + + + reload + Reload previous configuration if not confirmed + + + reboot + Reboot to saved configuration if not confirmed + + + (reload|reboot) + + + reboot + diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py index 920a19fec..851ac2134 100644 --- a/python/vyos/config_mgmt.py +++ b/python/vyos/config_mgmt.py @@ -33,6 +33,8 @@ from urllib.parse import urlunsplit from vyos.config import Config from vyos.configtree import ConfigTree from vyos.configtree import ConfigTreeError +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError from vyos.configtree import show_diff from vyos.load_config import load from vyos.load_config import LoadConfigError @@ -139,13 +141,19 @@ class ConfigMgmt: config = Config() d = config.get_config_dict( - ['system', 'config-management'], key_mangling=('-', '_'), get_first_key=True + ['system', 'config-management'], + key_mangling=('-', '_'), + get_first_key=True, + with_defaults=True, ) self.max_revisions = int(d.get('commit_revisions', 0)) self.num_revisions = 0 self.locations = d.get('commit_archive', {}).get('location', []) self.source_address = d.get('commit_archive', {}).get('source_address', '') + self.reboot_unconfirmed = bool(d.get('commit_confirm') == 'reboot') + self.config_dict = d + if config.exists(['system', 'host-name']): self.hostname = config.return_value(['system', 'host-name']) if config.exists(['system', 'domain-name']): @@ -175,42 +183,63 @@ class ConfigMgmt: def commit_confirm( self, minutes: int = DEFAULT_TIME_MINUTES, no_prompt: bool = False ) -> Tuple[str, int]: - """Commit with reboot to saved config in 'minutes' minutes if + """Commit with reload/reboot to saved config in 'minutes' minutes if 'confirm' call is not issued. """ if is_systemd_service_active(f'{timer_name}.timer'): msg = 'Another confirm is pending' return msg, 1 - if unsaved_commits(): + if self.reboot_unconfirmed and unsaved_commits(): W = '\nYou should save previous commits before commit-confirm !\n' else: W = '' - prompt_str = f""" + if self.reboot_unconfirmed: + prompt_str = f""" commit-confirm will automatically reboot in {minutes} minutes unless changes -are confirmed.\n +are confirmed. +Proceed ?""" + else: + prompt_str = f""" +commit-confirm will automatically reload previous config in {minutes} minutes +unless changes are confirmed. Proceed ?""" + prompt_str = W + prompt_str if not no_prompt and not ask_yes_no(prompt_str, default=True): msg = 'commit-confirm canceled' return msg, 1 - action = 'sg vyattacfg "/usr/bin/config-mgmt revert"' + if self.reboot_unconfirmed: + action = 'sg vyattacfg "/usr/bin/config-mgmt revert"' + else: + action = 'sg vyattacfg "/usr/bin/config-mgmt revert_soft"' + cmd = f'sudo systemd-run --quiet --on-active={minutes}m --unit={timer_name} {action}' rc, out = rc_cmd(cmd) if rc != 0: raise ConfigMgmtError(out) # start notify - cmd = f'sudo -b /usr/libexec/vyos/commit-confirm-notify.py {minutes}' + if self.reboot_unconfirmed: + cmd = ( + f'sudo -b /usr/libexec/vyos/commit-confirm-notify.py --reboot {minutes}' + ) + else: + cmd = f'sudo -b /usr/libexec/vyos/commit-confirm-notify.py {minutes}' + os.system(cmd) - msg = f'Initialized commit-confirm; {minutes} minutes to confirm before reboot' + if self.reboot_unconfirmed: + msg = f'Initialized commit-confirm; {minutes} minutes to confirm before reboot' + else: + msg = f'Initialized commit-confirm; {minutes} minutes to confirm before reload' + return msg, 0 def confirm(self) -> Tuple[str, int]: - """Do not reboot to saved config following 'commit-confirm'. + """Do not reboot/reload to saved/completed config following 'commit-confirm'. Update commit log and archive. """ if not is_systemd_service_active(f'{timer_name}.timer'): @@ -234,7 +263,11 @@ Proceed ?""" self._add_log_entry(**entry) self._update_archive() - msg = 'Reboot timer stopped' + if self.reboot_unconfirmed: + msg = 'Reboot timer stopped' + else: + msg = 'Reload timer stopped' + return msg, 0 def revert(self) -> Tuple[str, int]: @@ -248,6 +281,28 @@ Proceed ?""" return '', 0 + def revert_soft(self) -> Tuple[str, int]: + """Reload last revision, dropping commits from 'commit-confirm'.""" + _ = self._read_tmp_log_entry() + + # commits under commit-confirm are not added to revision list unless + # confirmed, hence a soft revert is to revision 0 + revert_ct = self._get_config_tree_revision(0) + + mask = os.umask(0o002) + session = ConfigSession(os.getpid(), app='config-mgmt') + + try: + session.load_explicit(revert_ct) + session.commit() + except ConfigSessionError as e: + raise ConfigMgmtError(e) from e + finally: + os.umask(mask) + del session + + return '', 0 + def rollback(self, rev: int, no_prompt: bool = False) -> Tuple[str, int]: """Reboot to config revision 'rev'.""" msg = '' @@ -684,7 +739,10 @@ Proceed ?""" entry = f.read() os.unlink(tmp_log_entry) except OSError as e: - logger.critical(f'error on file {tmp_log_entry}: {e}') + logger.info(f'error on file {tmp_log_entry}: {e}') + # fail gracefully in corner case: + # delete commit-revisions; commit-confirm + return {} return self._get_log_entry(entry) @@ -752,7 +810,8 @@ def run(): ) subparsers.add_parser('confirm', help='Confirm commit') - subparsers.add_parser('revert', help='Revert commit-confirm') + subparsers.add_parser('revert', help='Revert commit-confirm with reboot') + subparsers.add_parser('revert_soft', help='Revert commit-confirm with reload') rollback = subparsers.add_parser('rollback', help='Rollback to earlier config') rollback.add_argument('--rev', type=int, help='Revision number for rollback') diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index c0d3c7ecb..9c56d246a 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -268,6 +268,15 @@ class ConfigSession(object): out = self.__run_command(LOAD_CONFIG + [file_path]) return out + def load_explicit(self, file_path): + from vyos.load_config import load + from vyos.load_config import LoadConfigError + + try: + load(file_path, switch='explicit') + except LoadConfigError as e: + raise ConfigSessionError(e) from e + def migrate_and_load_config(self, file_path): out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path]) return out diff --git a/src/conf_mode/system_config-management.py b/src/conf_mode/system_config-management.py index 99f25bef6..a1ee136cd 100755 --- a/src/conf_mode/system_config-management.py +++ b/src/conf_mode/system_config-management.py @@ -38,7 +38,11 @@ def get_config(config=None): return mgmt -def verify(_mgmt): +def verify(mgmt): + d = mgmt.config_dict + if d.get('commit_confirm', '') == 'reload' and 'commit_revisions' not in d: + raise ConfigError('commit-confirm reload requires non-zero commit-revisions') + return diff --git a/src/helpers/commit-confirm-notify.py b/src/helpers/commit-confirm-notify.py index 69dda5112..af6167651 100755 --- a/src/helpers/commit-confirm-notify.py +++ b/src/helpers/commit-confirm-notify.py @@ -2,34 +2,56 @@ import os import sys import time +from argparse import ArgumentParser # Minutes before reboot to trigger notification. intervals = [1, 5, 15, 60] +parser = ArgumentParser() +parser.add_argument( + 'minutes', type=int, help='minutes before rollback to trigger notification' +) +parser.add_argument( + '--reboot', action='store_true', help="use 'soft' rollback instead of reboot" +) -def notify(interval): + +def notify(interval, reboot=False): s = '' if interval == 1 else 's' time.sleep((minutes - interval) * 60) - message = ( - '"[commit-confirm] System is going to reboot in ' - f'{interval} minute{s} to rollback the last commit.\n' - 'Confirm your changes to cancel the reboot."' - ) - os.system('wall -n ' + message) + if reboot: + message = ( + '"[commit-confirm] System will reboot in ' + f'{interval} minute{s}\nto rollback the last commit.\n' + 'Confirm your changes to cancel the reboot."' + ) + os.system('wall -n ' + message) + else: + message = ( + '"[commit-confirm] System will reload previous config in ' + f'{interval} minute{s}\nto rollback the last commit.\n' + 'Confirm your changes to cancel the reload."' + ) + os.system('wall -n ' + message) if __name__ == '__main__': # Must be run as root to call wall(1) without a banner. - if len(sys.argv) != 2 or os.getuid() != 0: + if os.getuid() != 0: print('This script requires superuser privileges.', file=sys.stderr) exit(1) - minutes = int(sys.argv[1]) + + args = parser.parse_args() + + minutes = args.minutes + reboot = args.reboot + # Drop the argument from the list so that the notification # doesn't kick in immediately. if minutes in intervals: intervals.remove(minutes) for interval in sorted(intervals, reverse=True): if minutes >= interval: - notify(interval) + notify(interval, reboot=reboot) minutes -= minutes - interval exit(0) -- cgit v1.2.3 From 64196ec5fd90a8caedef88edb9068a32e4e6abfa Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 7 Oct 2024 12:15:44 -0500 Subject: config-mgmt: T5976: move commit-confirm revert action to subnode --- .../system_config-management.xml.in | 43 +++++++++++++--------- src/conf_mode/system_config-management.py | 3 +- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/interface-definitions/system_config-management.xml.in b/interface-definitions/system_config-management.xml.in index b8fb6cdb5..a23d44aea 100644 --- a/interface-definitions/system_config-management.xml.in +++ b/interface-definitions/system_config-management.xml.in @@ -67,26 +67,33 @@ Number of revisions must be between 0 and 65535 - + - Commit confirm rollback type if no confirmation - - reload reboot - - - reload - Reload previous configuration if not confirmed - - - reboot - Reboot to saved configuration if not confirmed - - - (reload|reboot) - + Commit confirm options - reboot - + + + + Commit confirm revert action + + reload reboot + + + reload + Reload previous configuration if not confirmed + + + reboot + Reboot to saved configuration if not confirmed + + + (reload|reboot) + + + reboot + + + diff --git a/src/conf_mode/system_config-management.py b/src/conf_mode/system_config-management.py index a1ee136cd..8de4e5342 100755 --- a/src/conf_mode/system_config-management.py +++ b/src/conf_mode/system_config-management.py @@ -40,7 +40,8 @@ def get_config(config=None): def verify(mgmt): d = mgmt.config_dict - if d.get('commit_confirm', '') == 'reload' and 'commit_revisions' not in d: + confirm = d.get('commit_confirm', {}) + if confirm.get('action', '') == 'reload' and 'commit_revisions' not in d: raise ConfigError('commit-confirm reload requires non-zero commit-revisions') return -- cgit v1.2.3 From dfba19b3e89f47690c0abdee5f6410278237122e Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Tue, 8 Oct 2024 10:16:52 -0500 Subject: config-mgmt: T5976: display message when reverting to previous config --- python/vyos/config_mgmt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py index 851ac2134..1c2b70fdf 100644 --- a/python/vyos/config_mgmt.py +++ b/python/vyos/config_mgmt.py @@ -289,6 +289,9 @@ Proceed ?""" # confirmed, hence a soft revert is to revision 0 revert_ct = self._get_config_tree_revision(0) + message = '[commit-confirm] Reverting to previous config now' + os.system('wall -n ' + message) + mask = os.umask(0o002) session = ConfigSession(os.getpid(), app='config-mgmt') -- cgit v1.2.3 From bb28e001b1127eea3efbb7924c1871db3e8d2b82 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:15:20 +0200 Subject: pki: T6766: Add support for ECDSA private keys --- python/vyos/pki.py | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/python/vyos/pki.py b/python/vyos/pki.py index 5a0e2ddda..55dc02631 100644 --- a/python/vyos/pki.py +++ b/python/vyos/pki.py @@ -33,6 +33,8 @@ CERT_BEGIN='-----BEGIN CERTIFICATE-----\n' CERT_END='\n-----END CERTIFICATE-----' KEY_BEGIN='-----BEGIN PRIVATE KEY-----\n' KEY_END='\n-----END PRIVATE KEY-----' +KEY_EC_BEGIN='-----BEGIN EC PRIVATE KEY-----\n' +KEY_EC_END='\n-----END EC PRIVATE KEY-----' KEY_ENC_BEGIN='-----BEGIN ENCRYPTED PRIVATE KEY-----\n' KEY_ENC_END='\n-----END ENCRYPTED PRIVATE KEY-----' KEY_PUB_BEGIN='-----BEGIN PUBLIC KEY-----\n' @@ -228,8 +230,18 @@ def create_dh_parameters(bits=2048): def wrap_public_key(raw_data): return KEY_PUB_BEGIN + raw_data + KEY_PUB_END -def wrap_private_key(raw_data, passphrase=None): - return (KEY_ENC_BEGIN if passphrase else KEY_BEGIN) + raw_data + (KEY_ENC_END if passphrase else KEY_END) +def wrap_private_key(raw_data, passphrase=None, ec=False): + begin = KEY_BEGIN + end = KEY_END + + if passphrase: + begin = KEY_ENC_BEGIN + end = KEY_ENC_END + elif ec: + begin = KEY_EC_BEGIN + end = KEY_EC_END + + return begin + raw_data + end def wrap_openssh_public_key(raw_data, type): return f'{type} {raw_data}' @@ -262,17 +274,26 @@ def load_public_key(raw_data, wrap_tags=True): except ValueError: return False -def load_private_key(raw_data, passphrase=None, wrap_tags=True): - if wrap_tags: - raw_data = wrap_private_key(raw_data, passphrase) +def _load_private_key(raw_data, passphrase): + try: + return serialization.load_pem_private_key(bytes(raw_data, 'utf-8'), password=passphrase) + except (ValueError, TypeError): + return False +def load_private_key(raw_data, passphrase=None, wrap_tags=True): if passphrase is not None: passphrase = bytes(passphrase, 'utf-8') - try: - return serialization.load_pem_private_key(bytes(raw_data, 'utf-8'), password=passphrase) - except (ValueError, TypeError): + result = False + + if wrap_tags: + for ec_test in [False, True]: + wrapped_data = wrap_private_key(raw_data, passphrase, ec_test) + if result := _load_private_key(wrapped_data, passphrase): + return result return False + else: + return _load_private_key(raw_data, passphrase) def load_openssh_public_key(raw_data, type): try: -- cgit v1.2.3 From 90a4827284acd3cb072cdfeef323c522802c6449 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:55:11 +0200 Subject: haproxy: T6745: Rename `reverse-proxy` to `haproxy` --- data/config-mode-dependencies/vyos-1x.json | 2 +- data/op-mode-standardized.json | 2 +- data/templates/load-balancing/haproxy.cfg.j2 | 2 +- debian/control | 4 +- .../include/version/reverseproxy-version.xml.i | 2 +- .../load-balancing_haproxy.xml.in | 344 ++++++++++++++ .../load-balancing_reverse-proxy.xml.in | 344 -------------- op-mode-definitions/load-balacing_haproxy.in | 23 + op-mode-definitions/reverse-proxy.xml.in | 23 - .../scripts/cli/test_load-balancing_haproxy.py | 502 +++++++++++++++++++++ .../cli/test_load-balancing_reverse-proxy.py | 502 --------------------- src/conf_mode/load-balancing_haproxy.py | 206 +++++++++ src/conf_mode/load-balancing_reverse-proxy.py | 206 --------- src/conf_mode/pki.py | 2 +- src/migration-scripts/reverse-proxy/1-to-2 | 27 ++ src/op_mode/load-balancing_haproxy.py | 237 ++++++++++ src/op_mode/restart.py | 10 +- src/op_mode/reverseproxy.py | 237 ---------- 18 files changed, 1351 insertions(+), 1324 deletions(-) create mode 100644 interface-definitions/load-balancing_haproxy.xml.in delete mode 100644 interface-definitions/load-balancing_reverse-proxy.xml.in create mode 100644 op-mode-definitions/load-balacing_haproxy.in delete mode 100644 op-mode-definitions/reverse-proxy.xml.in create mode 100755 smoketest/scripts/cli/test_load-balancing_haproxy.py delete mode 100755 smoketest/scripts/cli/test_load-balancing_reverse-proxy.py create mode 100644 src/conf_mode/load-balancing_haproxy.py delete mode 100755 src/conf_mode/load-balancing_reverse-proxy.py create mode 100755 src/migration-scripts/reverse-proxy/1-to-2 create mode 100755 src/op_mode/load-balancing_haproxy.py delete mode 100755 src/op_mode/reverseproxy.py diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index 2981a0851..cbd14f7c6 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -26,10 +26,10 @@ "pki": { "ethernet": ["interfaces_ethernet"], "openvpn": ["interfaces_openvpn"], + "haproxy": ["load-balancing_haproxy"], "https": ["service_https"], "ipsec": ["vpn_ipsec"], "openconnect": ["vpn_openconnect"], - "reverse_proxy": ["load-balancing_reverse-proxy"], "rpki": ["protocols_rpki"], "sstp": ["vpn_sstp"], "sstpc": ["interfaces_sstpc"], diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index baa1e9110..35587b63c 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -25,7 +25,7 @@ "otp.py", "qos.py", "reset_vpn.py", -"reverseproxy.py", +"load-balancing_haproxy.py", "route.py", "storage.py", "system.py", diff --git a/data/templates/load-balancing/haproxy.cfg.j2 b/data/templates/load-balancing/haproxy.cfg.j2 index 5137966c1..786ebfb21 100644 --- a/data/templates/load-balancing/haproxy.cfg.j2 +++ b/data/templates/load-balancing/haproxy.cfg.j2 @@ -1,4 +1,4 @@ -### Autogenerated by load-balancing_reverse-proxy.py ### +### Autogenerated by load-balancing_haproxy.py ### global chroot /var/lib/haproxy diff --git a/debian/control b/debian/control index 15fb5d72e..20cfcdc43 100644 --- a/debian/control +++ b/debian/control @@ -202,9 +202,9 @@ Depends: # For "service router-advert" radvd, # End "service route-advert" -# For "load-balancing reverse-proxy" +# For "load-balancing haproxy" haproxy, -# End "load-balancing reverse-proxy" +# End "load-balancing haproxy" # For "load-balancing wan" vyatta-wanloadbalance, # End "load-balancing wan" diff --git a/interface-definitions/include/version/reverseproxy-version.xml.i b/interface-definitions/include/version/reverseproxy-version.xml.i index 907ea1e5e..4f09f2848 100644 --- a/interface-definitions/include/version/reverseproxy-version.xml.i +++ b/interface-definitions/include/version/reverseproxy-version.xml.i @@ -1,3 +1,3 @@ - + diff --git a/interface-definitions/load-balancing_haproxy.xml.in b/interface-definitions/load-balancing_haproxy.xml.in new file mode 100644 index 000000000..742272436 --- /dev/null +++ b/interface-definitions/load-balancing_haproxy.xml.in @@ -0,0 +1,344 @@ + + + + + + + Configure haproxy + 900 + + + + + Frontend service name + + #include + + Server name must be alphanumeric and can contain hyphen and underscores + + + + + Backend member + + #include + + Backend name must be alphanumeric and can contain hyphen and underscores + + txt + Name of haproxy backend system + + + load-balancing haproxy backend + + + + + #include + #include + #include + #include + #include + #include + #include + #include + + + Redirect HTTP to HTTPS + + + + + + SSL Certificate, SSL Key and CA + + + #include + + + + + + + Backend server name + + #include + + Backend name must be alphanumeric and can contain hyphen and underscores + + + + + Load-balancing algorithm + + source-address round-robin least-connection + + + source-address + Based on hash of source IP address + + + round-robin + Round robin + + + least-connection + Least connection + + + (source-address|round-robin|least-connection) + + + round-robin + + #include + #include + #include + #include + + + HTTP check configuration + + + + + HTTP method used for health check + + options head get post put + + + options|head|get|post|put + HTTP method used for health checking + + + (options|head|get|post|put) + + + + + + URI used for HTTP health check (Example: '/' or '/health') + + ^\/([^?#\s]*)(\?[^#\s]*)?$ + + + + + + Expected response for the health check to pass + + + + + Expected response status code for the health check to pass + + u32:200-399 + Expected response code + + + + + Status code must be in range 200-399 + + + + + Expected to be in response body for the health check to pass + + txt + A string expected to be in the response + + + + + + + + + + Non HTTP health check options + + ldap mysql pgsql redis smtp + + + ldap + LDAP protocol check + + + mysql + MySQL protocol check + + + pgsql + PostgreSQL protocol check + + + redis + Redis protocol check + + + smtp + SMTP protocol check + + + (ldap|mysql|redis|pgsql|smtp) + + + + #include + + + Backend server name + + + + + Backend server address + + ipv4 + IPv4 unicast peer address + + + ipv6 + IPv6 unicast peer address + + + + + + + + + Use backup server if other servers are not available + + + + + + Active health check backend server + + + + #include + + + Send a Proxy Protocol version 1 header (text format) + + + + + + Send a Proxy Protocol version 2 header (binary format) + + + + + + + + SSL Certificate, SSL Key and CA + + + #include + + + Do not attempt to verify SSL certificates for backend servers + + + + + + #include + + + + + Global perfomance parameters and limits + + + #include + + + Maximum allowed connections + + u32:1-2000000 + Maximum allowed connections + + + + + + + + + Cipher algorithms ("cipher suite") used during SSL/TLS handshake for all frontend servers + + ecdhe-ecdsa-aes128-gcm-sha256 ecdhe-rsa-aes128-gcm-sha256 ecdhe-ecdsa-aes256-gcm-sha384 ecdhe-rsa-aes256-gcm-sha384 ecdhe-ecdsa-chacha20-poly1305 ecdhe-rsa-chacha20-poly1305 dhe-rsa-aes128-gcm-sha256 dhe-rsa-aes256-gcm-sha384 + + + ecdhe-ecdsa-aes128-gcm-sha256 + ecdhe-ecdsa-aes128-gcm-sha256 + + + ecdhe-rsa-aes128-gcm-sha256 + ecdhe-rsa-aes128-gcm-sha256 + + + ecdhe-ecdsa-aes256-gcm-sha384 + ecdhe-ecdsa-aes256-gcm-sha384 + + + ecdhe-rsa-aes256-gcm-sha384 + ecdhe-rsa-aes256-gcm-sha384 + + + ecdhe-ecdsa-chacha20-poly1305 + ecdhe-ecdsa-chacha20-poly1305 + + + ecdhe-rsa-chacha20-poly1305 + ecdhe-rsa-chacha20-poly1305 + + + dhe-rsa-aes128-gcm-sha256 + dhe-rsa-aes128-gcm-sha256 + + + dhe-rsa-aes256-gcm-sha384 + dhe-rsa-aes256-gcm-sha384 + + + (ecdhe-ecdsa-aes128-gcm-sha256|ecdhe-rsa-aes128-gcm-sha256|ecdhe-ecdsa-aes256-gcm-sha384|ecdhe-rsa-aes256-gcm-sha384|ecdhe-ecdsa-chacha20-poly1305|ecdhe-rsa-chacha20-poly1305|dhe-rsa-aes128-gcm-sha256|dhe-rsa-aes256-gcm-sha384) + + + + ecdhe-ecdsa-aes128-gcm-sha256 ecdhe-rsa-aes128-gcm-sha256 ecdhe-ecdsa-aes256-gcm-sha384 ecdhe-rsa-aes256-gcm-sha384 ecdhe-ecdsa-chacha20-poly1305 ecdhe-rsa-chacha20-poly1305 dhe-rsa-aes128-gcm-sha256 dhe-rsa-aes256-gcm-sha384 + + + + Specify the minimum required TLS version + + 1.2 1.3 + + + 1.2 + TLS v1.2 + + + 1.3 + TLS v1.3 + + + (1.2|1.3) + + + 1.3 + + + + #include + + + + + diff --git a/interface-definitions/load-balancing_reverse-proxy.xml.in b/interface-definitions/load-balancing_reverse-proxy.xml.in deleted file mode 100644 index 18274622c..000000000 --- a/interface-definitions/load-balancing_reverse-proxy.xml.in +++ /dev/null @@ -1,344 +0,0 @@ - - - - - - - Configure reverse-proxy - 900 - - - - - Frontend service name - - #include - - Server name must be alphanumeric and can contain hyphen and underscores - - - - - Backend member - - #include - - Backend name must be alphanumeric and can contain hyphen and underscores - - txt - Name of reverse-proxy backend system - - - load-balancing reverse-proxy backend - - - - - #include - #include - #include - #include - #include - #include - #include - #include - - - Redirect HTTP to HTTPS - - - - - - SSL Certificate, SSL Key and CA - - - #include - - - - - - - Backend server name - - #include - - Backend name must be alphanumeric and can contain hyphen and underscores - - - - - Load-balancing algorithm - - source-address round-robin least-connection - - - source-address - Based on hash of source IP address - - - round-robin - Round robin - - - least-connection - Least connection - - - (source-address|round-robin|least-connection) - - - round-robin - - #include - #include - #include - #include - - - HTTP check configuration - - - - - HTTP method used for health check - - options head get post put - - - options|head|get|post|put - HTTP method used for health checking - - - (options|head|get|post|put) - - - - - - URI used for HTTP health check (Example: '/' or '/health') - - ^\/([^?#\s]*)(\?[^#\s]*)?$ - - - - - - Expected response for the health check to pass - - - - - Expected response status code for the health check to pass - - u32:200-399 - Expected response code - - - - - Status code must be in range 200-399 - - - - - Expected to be in response body for the health check to pass - - txt - A string expected to be in the response - - - - - - - - - - Non HTTP health check options - - ldap mysql pgsql redis smtp - - - ldap - LDAP protocol check - - - mysql - MySQL protocol check - - - pgsql - PostgreSQL protocol check - - - redis - Redis protocol check - - - smtp - SMTP protocol check - - - (ldap|mysql|redis|pgsql|smtp) - - - - #include - - - Backend server name - - - - - Backend server address - - ipv4 - IPv4 unicast peer address - - - ipv6 - IPv6 unicast peer address - - - - - - - - - Use backup server if other servers are not available - - - - - - Active health check backend server - - - - #include - - - Send a Proxy Protocol version 1 header (text format) - - - - - - Send a Proxy Protocol version 2 header (binary format) - - - - - - - - SSL Certificate, SSL Key and CA - - - #include - - - Do not attempt to verify SSL certificates for backend servers - - - - - - #include - - - - - Global perfomance parameters and limits - - - #include - - - Maximum allowed connections - - u32:1-2000000 - Maximum allowed connections - - - - - - - - - Cipher algorithms ("cipher suite") used during SSL/TLS handshake for all frontend servers - - ecdhe-ecdsa-aes128-gcm-sha256 ecdhe-rsa-aes128-gcm-sha256 ecdhe-ecdsa-aes256-gcm-sha384 ecdhe-rsa-aes256-gcm-sha384 ecdhe-ecdsa-chacha20-poly1305 ecdhe-rsa-chacha20-poly1305 dhe-rsa-aes128-gcm-sha256 dhe-rsa-aes256-gcm-sha384 - - - ecdhe-ecdsa-aes128-gcm-sha256 - ecdhe-ecdsa-aes128-gcm-sha256 - - - ecdhe-rsa-aes128-gcm-sha256 - ecdhe-rsa-aes128-gcm-sha256 - - - ecdhe-ecdsa-aes256-gcm-sha384 - ecdhe-ecdsa-aes256-gcm-sha384 - - - ecdhe-rsa-aes256-gcm-sha384 - ecdhe-rsa-aes256-gcm-sha384 - - - ecdhe-ecdsa-chacha20-poly1305 - ecdhe-ecdsa-chacha20-poly1305 - - - ecdhe-rsa-chacha20-poly1305 - ecdhe-rsa-chacha20-poly1305 - - - dhe-rsa-aes128-gcm-sha256 - dhe-rsa-aes128-gcm-sha256 - - - dhe-rsa-aes256-gcm-sha384 - dhe-rsa-aes256-gcm-sha384 - - - (ecdhe-ecdsa-aes128-gcm-sha256|ecdhe-rsa-aes128-gcm-sha256|ecdhe-ecdsa-aes256-gcm-sha384|ecdhe-rsa-aes256-gcm-sha384|ecdhe-ecdsa-chacha20-poly1305|ecdhe-rsa-chacha20-poly1305|dhe-rsa-aes128-gcm-sha256|dhe-rsa-aes256-gcm-sha384) - - - - ecdhe-ecdsa-aes128-gcm-sha256 ecdhe-rsa-aes128-gcm-sha256 ecdhe-ecdsa-aes256-gcm-sha384 ecdhe-rsa-aes256-gcm-sha384 ecdhe-ecdsa-chacha20-poly1305 ecdhe-rsa-chacha20-poly1305 dhe-rsa-aes128-gcm-sha256 dhe-rsa-aes256-gcm-sha384 - - - - Specify the minimum required TLS version - - 1.2 1.3 - - - 1.2 - TLS v1.2 - - - 1.3 - TLS v1.3 - - - (1.2|1.3) - - - 1.3 - - - - #include - - - - - diff --git a/op-mode-definitions/load-balacing_haproxy.in b/op-mode-definitions/load-balacing_haproxy.in new file mode 100644 index 000000000..c3d6c799b --- /dev/null +++ b/op-mode-definitions/load-balacing_haproxy.in @@ -0,0 +1,23 @@ + + + + + + + Restart haproxy service + + sudo ${vyos_op_scripts_dir}/restart.py restart_service --name haproxy + + + + + + + + Show load-balancing haproxy + + sudo ${vyos_op_scripts_dir}/load-balacing_haproxy.py show + + + + diff --git a/op-mode-definitions/reverse-proxy.xml.in b/op-mode-definitions/reverse-proxy.xml.in deleted file mode 100644 index b45ce107f..000000000 --- a/op-mode-definitions/reverse-proxy.xml.in +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - Restart reverse-proxy service - - sudo ${vyos_op_scripts_dir}/restart.py restart_service --name reverse_proxy - - - - - - - - Show load-balancing reverse-proxy - - sudo ${vyos_op_scripts_dir}/reverseproxy.py show - - - - diff --git a/smoketest/scripts/cli/test_load-balancing_haproxy.py b/smoketest/scripts/cli/test_load-balancing_haproxy.py new file mode 100755 index 000000000..967eb3869 --- /dev/null +++ b/smoketest/scripts/cli/test_load-balancing_haproxy.py @@ -0,0 +1,502 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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 . + +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.utils.process import process_named_running +from vyos.utils.file import read_file + +PROCESS_NAME = 'haproxy' +HAPROXY_CONF = '/run/haproxy/haproxy.cfg' +base_path = ['load-balancing', 'haproxy'] +proxy_interface = 'eth1' + +valid_ca_cert = """ +MIIDnTCCAoWgAwIBAgIUewSDtLiZbhg1YEslMnqRl1shoPcwDQYJKoZIhvcNAQEL +BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM +CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0y +NDA0MDEwNTQ3MzJaFw0yOTAzMzEwNTQ3MzJaMFcxCzAJBgNVBAYTAkdCMRMwEQYD +VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5 +T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC/D6W27rfpdPIf16JHs8fx/7VehyCk8m03dPAQqv6wQiHF5xhXaFZER1+c +nf7oExp9zi/4HJ/KRbcc1loVArXtV0zwAUftBmUeezGVfxhCHKhP89GnV4NB97jj +klHFSxjEoT/0YvJQ1IV/3Cos1T5O8x14WIi31l7WQGYAyWxUXiP8QxGVmF3odEJo +O3e7Ew9HFkamvuL6Z6c4uAVMM7uYXme7q0OM49Wu7C9hj39ZKbjG5FFKZTj+zDKg +SbOiQaFk3blOky/e3ifNjZelGtussYPOMBkUirLvrSGGy7s3lm8Yp5PH5+UkVQB2 +rZyxRdZTC9kh+dShR1s/qcPnDw7lAgMBAAGjYTBfMA8GA1UdEwEB/wQFMAMBAf8w +DgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAd +BgNVHQ4EFgQU/HE2UPn8JQB/9EL52GquPxZqr5MwDQYJKoZIhvcNAQELBQADggEB +AIkMmqyoMqidTa3lvUPJNl4H+Ef/yPQkTkrsOd3WL8DQysyUdMLdQozr3K1bH5XB +wRxoXX211nu4WhN18LsFJRCuHBSxmaNkBGFyl+JNvhPUSI6j0somNMCS75KJ0ZDx +2HZsXmmJFF902VQxCR7vCIrFDrKDYq1e7GQbFS8t46FlpqivQMQWNPt18Bthj/1Y +lO2GKRWFCX8VlOW7FtDQ6B3oC1oAGHBBGogAx7/0gh9DnYBKT14V/kuWW3RNABZJ +ewHO1C6icQdnjtaREDyTP4oyL+uyAfXrFfbpti2hc00f8oYPQZYxj1yxl4UAdNij +mS6YqH/WRioGMe3tBVeSdoo= +""" + +valid_ca_private_key = """ +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/D6W27rfpdPIf +16JHs8fx/7VehyCk8m03dPAQqv6wQiHF5xhXaFZER1+cnf7oExp9zi/4HJ/KRbcc +1loVArXtV0zwAUftBmUeezGVfxhCHKhP89GnV4NB97jjklHFSxjEoT/0YvJQ1IV/ +3Cos1T5O8x14WIi31l7WQGYAyWxUXiP8QxGVmF3odEJoO3e7Ew9HFkamvuL6Z6c4 +uAVMM7uYXme7q0OM49Wu7C9hj39ZKbjG5FFKZTj+zDKgSbOiQaFk3blOky/e3ifN +jZelGtussYPOMBkUirLvrSGGy7s3lm8Yp5PH5+UkVQB2rZyxRdZTC9kh+dShR1s/ +qcPnDw7lAgMBAAECggEAGm+j0kf9koPn7Jf9kEZD6CwlgEraLXiNvBqmDOhcDS9Z +VPTA3XdGWHQ3uofx+VKLW9TntkDfqzEyQP83v6h8W7a0opDKzvUPkMQi/Dh1ttAY +SdfGrozhUINiRbq9LbtSVgKpwrreJGkDf8mK3GE1Gd9xuHEnmahDvwlyE7HLF3Eh +2xJDSAPx3OxcjR5hW7vbojhVCyCfuYTlZB86f0Sb8SqxZMt/y2zKmbzoTqpUBWbg +lBnE7GJoNR07DWjxvEP8r6kQMh670I01SUR42CSK8X8asHhhZHUcggsNno+BBc6K +sy4HzDIYIay6oy0atcVzKsGrlNCveeAiSEcw7x2yAQKBgQDsXz2FbhXYV5Vbt4wU +5EWOa7if/+FG+TcVezOF3xlNBgykjXHQaYTYHrJq0qsEFrNT3ZGm9ezY4LdF3BTt +5z/+i8QlCCw/nr3N7JZx6U5+OJl1j3NLFoFx3+DXo31pgJJEQCHHwdCkF5IuOcZ/ +b3nXkRZ80BVv7XD6F9bMHEwLYQKBgQDO7THcRDbsE6/+7VsTDf0P/JENba3DBBu1 +gjb1ItL5FHJwMgnkUadRZRo0QKye848ugribed39qSoJfNaBJrAT5T8S/9q+lXft +vXUckcBO1CKNaP9gqF5fPIdNHf64GbmCiiHjOTE3rwJjkxJPpzLXyvgBO4aLeesK +ThBdW+iWBQKBgD3crz08knsMcQqP/xl4pLuhdbBqR4tLrh7xH4rp2LVP3/8xBZiG +BT6Kyicq+5cWWdiZJIWN127rYQvnjZK18wmriqomeW4tHX/Ha5hkdyaRqZga8xGz +0iz7at0E7M2v2JgEMNMW5oQLpzZx6IFxq3G/hyMjUnj4q5jIpG7G+SABAoGBAKgT +8Ika+4WcpDssrup2VVTT8Tp4GUkroBo6D8vkInvhiObrLi+/x2mM9tD0q4JdEbNU +yQC454EwFA4q0c2MED/I2QfkvNhLbmO0nVi8ZvlgxEQawjzP5f/zmW8haxI9Cvsm +mkoH3Zt+UzFwd9ItXFX97p6JrErEmA8Bw7chfXXFAoGACWR/c+s7hnX6gzyah3N1 +Db0xAaS6M9fzogcg2OM1i/6OCOcp4Sh1fmPG7tN45CCnFkhgVoRkSSA5MJAe2I/r +xFm72VX7567T+4qIFua2iDxIBA/Z4zmj+RYfhHGPYZjdSjprKJxY6QOv5aoluBvE +mlLy1Hmcry+ukWZtWezZfGY= +""" + +valid_cert = """ +MIIDsTCCApmgAwIBAgIUDKOfYIwwtjww0vAMvJnXnGLhL+0wDQYJKoZIhvcNAQEL +BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM +CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0y +NDA0MDEwNTQ5NTdaFw0yNTA0MDEwNTQ5NTdaMFcxCzAJBgNVBAYTAkdCMRMwEQYD +VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5 +T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCHtW25Umt6rqm2gfzqAZg1/VsqefZwAqIUAm2T3VwHQZ/2tNdr8ROWASii +W5PToC7N8StMwFl2YoIof+MXGMO00toTTJePZOJKjF9U9hL3kuYuY1+yng4fl+E0 +96xVobb2KY4lMZ2rVwmpB7jkNO2LWxbJ6vHKcwMOhlx/8NEKIoVmkBT1Zkgy5dgn +PgTtJcdVIU75XhQWqBmAUsMmACuZfqSYJbAv3hHz5V+Ejt0dI6mlGM7TXsCC9tKM +64paIKZooFm78IsxJ26jHpZ8eh+SDBz0VBydBFWXm8VhOJ8NlZ1opAh3AWxFZDGt +49uOsy82VmUcHPyoZ8DKYkBFHfSpAgMBAAGjdTBzMAwGA1UdEwEB/wQCMAAwDgYD +VR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBTeTcgM +pRxAMjVBirjzo2QUu5H5fzAfBgNVHSMEGDAWgBT8cTZQ+fwlAH/0QvnYaq4/Fmqv +kzANBgkqhkiG9w0BAQsFAAOCAQEAi4dBcH7TIYwWRW6bWRubMA7ztonV4EYb15Zf +9yNafMWAEEBOii/DFo+j/ky9oInl7ZHw7gTIyXfLEarX/bM6fHOgiyj4zp3u6RnH +5qlBypu/YCnyPjE/GvV05m2rrXnxZ4rCtcoO4u/HyGbV+jGnCmjShKICKyu1FdMd +eeZRrLKPO/yghadGH34WVQnrbaorwlbi+NjB6fxmZQx5HE/SyK/9sb6WCpLMGHoy +MpdQo3lV1ewtL3ElIWDq6mO030Mo5pwpjIU+8yHHNBVzg6mlGVgQPAp0gbUei9aP +CJ8SLmMEi3NDk0E/sPgVC17e6bf2bx2nRuXROZekG2dd90Iu8g== +""" + +valid_cert_private_key = """ +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCHtW25Umt6rqm2 +gfzqAZg1/VsqefZwAqIUAm2T3VwHQZ/2tNdr8ROWASiiW5PToC7N8StMwFl2YoIo +f+MXGMO00toTTJePZOJKjF9U9hL3kuYuY1+yng4fl+E096xVobb2KY4lMZ2rVwmp +B7jkNO2LWxbJ6vHKcwMOhlx/8NEKIoVmkBT1Zkgy5dgnPgTtJcdVIU75XhQWqBmA +UsMmACuZfqSYJbAv3hHz5V+Ejt0dI6mlGM7TXsCC9tKM64paIKZooFm78IsxJ26j +HpZ8eh+SDBz0VBydBFWXm8VhOJ8NlZ1opAh3AWxFZDGt49uOsy82VmUcHPyoZ8DK +YkBFHfSpAgMBAAECggEABofhw0W/ACEMcAjmpNTFkFCUXPGQXWDVD7EzuIZSNdOv +yOm4Rbys6H6/B7wwO6KVagoBf1Cw5Xh1YtFPuoZxsZ+liMD6eLc+SB/j/RTYAhPO +0bvsyK3gSF8w4nGKWLce9M74ZRwThkG6qGijmlDdPyP3r2kn8GoTQzVOWYZbavk/ +H3uE6PsZSWjOY+Mnm3vEmeItPYKGZ5+IP+YiTqZ4NCggBwH7csnR3/kbwY5Ns7jl +3Av+EAdIeUwDNeMfLTzN7GphJR7gL6YQIhGKxE+W0GHXL2FubnnrFx8G75HFh1ay +GkJXEqY5Lbd+7VPS0KcQdwhMSSoJsY5GUORUqrU80QKBgQC/0wJSu+Gfe7dONIby +mnGRppSRIQVRjCjbVIN+Y2h1Kp3aK0qDpV7KFLCiUUtz9rWHR/NB4cDaIW543T55 +/jXUMD2j3EqtbtlsVQfDLQV7DyDrMmBAs4REHmyZmWTzHjCDUO79ahdOlZs34Alz +wfpX3L3WVYGIAJKZtsUZ8FbrGQKBgQC1HFgVZ1PqP9/pW50RMh06BbQrhWPGiWgH +Rn5bFthLkp3uqr9bReBq9tu3sqJuAhFudH68wup+Z+fTcHAcNg2Rs+Q+IKnULdB/ +UQHYoPjeWOvHAuOmgn9iD9OD7GCIv8fZmLit09vAsOWq+NKNBKCknGM70CDrvAlQ +lOAUa34YEQKBgQC5i8GThWiYe3Kzktt1jy6LVDYgq3AZkRl0Diui9UT1EGPfxEAv +VqZ5kcnJOBlj8h9k25PRBi0k0XGqN1dXaS1oMcFt3ofdenuU7iqz/7htcBTHa9Lu +wrYNreAeMuISyADlBEQnm5cvzEZ3pZ1++wLMOhjmWY8Rnnwvczrz/CYXAQKBgH+t +vcNJFvWblkUzWuWWiNgw0TWlUhPTJs2KOuYIku+kK0bohQLZnj6KTZeRjcU0HAnc +gsScPShkJCEBsWeSC7reMVhDOrbknYpEF6MayJgn5ABm3wqyEQ+WzKzCZcPCQCf8 +7KVPKCsOCrufsv/LdVzXC3ZNYggOhhqS+e4rYbehAoGBAIsq252o3vgrunzS5FZx +IONA2FvYrxVbDn5aF8WfNSdKFy3CAlt0P+Fm8gYbrKylIfMXpL8Oqc9RJou5onZP +ZXLrtgVJR9W020qTurO2f91qfU8646n11hR9ObBB1IYbagOU0Pw1Nrq/FRp/u2tx +7i7xFz2WEiQeSCPaKYOiqM3t +""" + + +class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): + def tearDown(self): + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + self.cli_delete(['interfaces', 'ethernet', proxy_interface, 'address']) + self.cli_delete(base_path) + self.cli_delete(['pki']) + self.cli_commit() + + # Process must be terminated after deleting the config + self.assertFalse(process_named_running(PROCESS_NAME)) + + def base_config(self): + self.cli_set(base_path + ['service', 'https_front', 'mode', 'http']) + self.cli_set(base_path + ['service', 'https_front', 'port', '4433']) + self.cli_set(base_path + ['service', 'https_front', 'backend', 'bk-01']) + + self.cli_set(base_path + ['backend', 'bk-01', 'mode', 'http']) + self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'address', '192.0.2.11']) + self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'port', '9090']) + self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'send-proxy']) + + self.cli_set(base_path + ['global-parameters', 'max-connections', '1000']) + + def configure_pki(self): + + # Valid CA + self.cli_set(['pki', 'ca', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')]) + self.cli_set(['pki', 'ca', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')]) + + # Valid cert + self.cli_set(['pki', 'certificate', 'smoketest', 'certificate', valid_cert.replace('\n','')]) + self.cli_set(['pki', 'certificate', 'smoketest', 'private', 'key', valid_cert_private_key.replace('\n','')]) + + def test_01_lb_reverse_proxy_domain(self): + domains_bk_first = ['n1.example.com', 'n2.example.com', 'n3.example.com'] + domain_bk_second = 'n5.example.com' + frontend = 'https_front' + front_port = '4433' + bk_server_first = '192.0.2.11' + bk_server_second = '192.0.2.12' + bk_first_name = 'bk-01' + bk_second_name = 'bk-02' + bk_server_port = '9090' + mode = 'http' + rule_ten = '10' + rule_twenty = '20' + rule_thirty = '30' + send_proxy = 'send-proxy' + max_connections = '1000' + + back_base = base_path + ['backend'] + + self.cli_set(base_path + ['service', frontend, 'mode', mode]) + self.cli_set(base_path + ['service', frontend, 'port', front_port]) + for domain in domains_bk_first: + self.cli_set(base_path + ['service', frontend, 'rule', rule_ten, 'domain-name', domain]) + self.cli_set(base_path + ['service', frontend, 'rule', rule_ten, 'set', 'backend', bk_first_name]) + self.cli_set(base_path + ['service', frontend, 'rule', rule_twenty, 'domain-name', domain_bk_second]) + self.cli_set(base_path + ['service', frontend, 'rule', rule_twenty, 'set', 'backend', bk_second_name]) + self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'url-path', 'end', '/test']) + self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'set', 'backend', bk_second_name]) + + self.cli_set(back_base + [bk_first_name, 'mode', mode]) + self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, 'address', bk_server_first]) + self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, 'port', bk_server_port]) + self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, send_proxy]) + + self.cli_set(back_base + [bk_second_name, 'mode', mode]) + self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'address', bk_server_second]) + self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'port', bk_server_port]) + self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'backup']) + + self.cli_set(base_path + ['global-parameters', 'max-connections', max_connections]) + + # commit changes + self.cli_commit() + + config = read_file(HAPROXY_CONF) + + # Global + self.assertIn(f'maxconn {max_connections}', config) + + # Frontend + self.assertIn(f'frontend {frontend}', config) + self.assertIn(f'bind [::]:{front_port} v4v6', config) + self.assertIn(f'mode {mode}', config) + for domain in domains_bk_first: + self.assertIn(f'acl {rule_ten} hdr(host) -i {domain}', config) + self.assertIn(f'use_backend {bk_first_name} if {rule_ten}', config) + self.assertIn(f'acl {rule_twenty} hdr(host) -i {domain_bk_second}', config) + self.assertIn(f'use_backend {bk_second_name} if {rule_twenty}', config) + self.assertIn(f'acl {rule_thirty} path -i -m end /test', config) + self.assertIn(f'use_backend {bk_second_name} if {rule_thirty}', config) + + # Backend + self.assertIn(f'backend {bk_first_name}', config) + self.assertIn(f'balance roundrobin', config) + self.assertIn(f'option forwardfor', config) + self.assertIn('http-request add-header X-Forwarded-Proto https if { ssl_fc }', config) + self.assertIn(f'mode {mode}', config) + self.assertIn(f'server {bk_first_name} {bk_server_first}:{bk_server_port} send-proxy', config) + + self.assertIn(f'backend {bk_second_name}', config) + self.assertIn(f'mode {mode}', config) + self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port}', config) + self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port} backup', config) + + def test_02_lb_reverse_proxy_cert_not_exists(self): + self.base_config() + self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert']) + + with self.assertRaises(ConfigSessionError) as e: + self.cli_commit() + # self.assertIn('\nCertificates does not exist in PKI\n', str(e.exception)) + + self.cli_delete(base_path) + self.configure_pki() + + self.base_config() + self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert']) + + with self.assertRaises(ConfigSessionError) as e: + self.cli_commit() + # self.assertIn('\nCertificate "cert" does not exist\n', str(e.exception)) + + self.cli_delete(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert']) + self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'smoketest']) + self.cli_commit() + + def test_03_lb_reverse_proxy_ca_not_exists(self): + self.base_config() + self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test']) + + with self.assertRaises(ConfigSessionError) as e: + self.cli_commit() + # self.assertIn('\nCA certificates does not exist in PKI\n', str(e.exception)) + + self.cli_delete(base_path) + self.configure_pki() + + self.base_config() + self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test']) + + with self.assertRaises(ConfigSessionError) as e: + self.cli_commit() + # self.assertIn('\nCA certificate "ca-test" does not exist\n', str(e.exception)) + + self.cli_delete(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test']) + self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest']) + self.cli_commit() + + def test_04_lb_reverse_proxy_backend_ssl_no_verify(self): + # Setup base + self.configure_pki() + self.base_config() + + # Set no-verify option + self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'no-verify']) + self.cli_commit() + + # Test no-verify option + config = read_file(HAPROXY_CONF) + self.assertIn('server bk-01 192.0.2.11:9090 send-proxy ssl verify none', config) + + # Test setting ca-certificate alongside no-verify option fails, to test config validation + self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest']) + with self.assertRaises(ConfigSessionError) as e: + self.cli_commit() + + def test_05_lb_reverse_proxy_backend_http_check(self): + # Setup base + self.base_config() + + # Set http-check + self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'method', 'get']) + self.cli_commit() + + # Test http-check + config = read_file(HAPROXY_CONF) + self.assertIn('option httpchk', config) + self.assertIn('http-check send meth GET', config) + + # Set http-check with uri and status + self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'uri', '/health']) + self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200']) + self.cli_commit() + + # Test http-check with uri and status + config = read_file(HAPROXY_CONF) + self.assertIn('option httpchk', config) + self.assertIn('http-check send meth GET uri /health', config) + self.assertIn('http-check expect status 200', config) + + # Set http-check with string + self.cli_delete(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200']) + self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'string', 'success']) + self.cli_commit() + + # Test http-check with string + config = read_file(HAPROXY_CONF) + self.assertIn('option httpchk', config) + self.assertIn('http-check send meth GET uri /health', config) + self.assertIn('http-check expect string success', config) + + # Test configuring both http-check & health-check fails validation script + self.cli_set(base_path + ['backend', 'bk-01', 'health-check', 'ldap']) + with self.assertRaises(ConfigSessionError) as e: + self.cli_commit() + + def test_06_lb_reverse_proxy_tcp_mode(self): + frontend = 'tcp_8443' + mode = 'tcp' + front_port = '8433' + tcp_request_delay = "5000" + rule_thirty = '30' + domain_bk = 'n6.example.com' + ssl_opt = "req-ssl-sni" + bk_name = 'bk-03' + bk_server = '192.0.2.11' + bk_server_port = '9090' + + back_base = base_path + ['backend'] + + self.cli_set(base_path + ['service', frontend, 'mode', mode]) + self.cli_set(base_path + ['service', frontend, 'port', front_port]) + self.cli_set(base_path + ['service', frontend, 'tcp-request', 'inspect-delay', tcp_request_delay]) + + self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'domain-name', domain_bk]) + self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'ssl', ssl_opt]) + self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'set', 'backend', bk_name]) + + self.cli_set(back_base + [bk_name, 'mode', mode]) + self.cli_set(back_base + [bk_name, 'server', bk_name, 'address', bk_server]) + self.cli_set(back_base + [bk_name, 'server', bk_name, 'port', bk_server_port]) + + # commit changes + self.cli_commit() + + config = read_file(HAPROXY_CONF) + + # Frontend + self.assertIn(f'frontend {frontend}', config) + self.assertIn(f'bind [::]:{front_port} v4v6', config) + self.assertIn(f'mode {mode}', config) + + self.assertIn(f'tcp-request inspect-delay {tcp_request_delay}', config) + self.assertIn(f"tcp-request content accept if {{ req_ssl_hello_type 1 }}", config) + self.assertIn(f'acl {rule_thirty} req_ssl_sni -i {domain_bk}', config) + self.assertIn(f'use_backend {bk_name} if {rule_thirty}', config) + + # Backend + self.assertIn(f'backend {bk_name}', config) + self.assertIn(f'balance roundrobin', config) + self.assertIn(f'mode {mode}', config) + self.assertIn(f'server {bk_name} {bk_server}:{bk_server_port}', config) + + def test_07_lb_reverse_proxy_http_response_headers(self): + # Setup base + self.configure_pki() + self.base_config() + + # Set example headers in both frontend and backend + self.cli_set(base_path + ['service', 'https_front', 'http-response-headers', 'Cache-Control', 'value', 'max-age=604800']) + self.cli_set(base_path + ['backend', 'bk-01', 'http-response-headers', 'Proxy-Backend-ID', 'value', 'bk-01']) + self.cli_commit() + + # Test headers are present in generated configuration file + config = read_file(HAPROXY_CONF) + self.assertIn('http-response set-header Cache-Control \'max-age=604800\'', config) + self.assertIn('http-response set-header Proxy-Backend-ID \'bk-01\'', config) + + # Test setting alongside modes other than http is blocked by validation conditions + self.cli_set(base_path + ['service', 'https_front', 'mode', 'tcp']) + with self.assertRaises(ConfigSessionError) as e: + self.cli_commit() + + def test_08_lb_reverse_proxy_tcp_health_checks(self): + # Setup PKI + self.configure_pki() + + # Define variables + frontend = 'fe_ldaps' + mode = 'tcp' + health_check = 'ldap' + front_port = '636' + bk_name = 'bk_ldap' + bk_servers = ['192.0.2.11', '192.0.2.12'] + bk_server_port = '389' + + # Configure frontend + self.cli_set(base_path + ['service', frontend, 'mode', mode]) + self.cli_set(base_path + ['service', frontend, 'port', front_port]) + self.cli_set(base_path + ['service', frontend, 'ssl', 'certificate', 'smoketest']) + + # Configure backend + self.cli_set(base_path + ['backend', bk_name, 'mode', mode]) + self.cli_set(base_path + ['backend', bk_name, 'health-check', health_check]) + for index, bk_server in enumerate(bk_servers): + self.cli_set(base_path + ['backend', bk_name, 'server', f'srv-{index}', 'address', bk_server]) + self.cli_set(base_path + ['backend', bk_name, 'server', f'srv-{index}', 'port', bk_server_port]) + + # Commit & read config + self.cli_commit() + config = read_file(HAPROXY_CONF) + + # Validate Frontend + self.assertIn(f'frontend {frontend}', config) + self.assertIn(f'bind [::]:{front_port} v4v6 ssl crt /run/haproxy/smoketest.pem', config) + self.assertIn(f'mode {mode}', config) + self.assertIn(f'backend {bk_name}', config) + + # Validate Backend + self.assertIn(f'backend {bk_name}', config) + self.assertIn(f'option {health_check}-check', config) + self.assertIn(f'mode {mode}', config) + for index, bk_server in enumerate(bk_servers): + self.assertIn(f'server srv-{index} {bk_server}:{bk_server_port}', config) + + # Validate SMTP option renders correctly + self.cli_set(base_path + ['backend', bk_name, 'health-check', 'smtp']) + self.cli_commit() + config = read_file(HAPROXY_CONF) + self.assertIn(f'option smtpchk', config) + + def test_09_lb_reverse_proxy_logging(self): + # Setup base + self.base_config() + self.cli_commit() + + # Ensure default logging configuration is present + config = read_file(HAPROXY_CONF) + + # Test global-parameters logging options + self.cli_set(base_path + ['global-parameters', 'logging', 'facility', 'local1', 'level', 'err']) + self.cli_set(base_path + ['global-parameters', 'logging', 'facility', 'local2', 'level', 'warning']) + self.cli_commit() + + # Test global logging parameters are generated in configuration file + config = read_file(HAPROXY_CONF) + self.assertIn('log /dev/log local1 err', config) + self.assertIn('log /dev/log local2 warning', config) + + # Test backend logging options + backend_path = base_path + ['backend', 'bk-01'] + self.cli_set(backend_path + ['logging', 'facility', 'local3', 'level', 'debug']) + self.cli_set(backend_path + ['logging', 'facility', 'local4', 'level', 'info']) + self.cli_commit() + + # Test backend logging parameters are generated in configuration file + config = read_file(HAPROXY_CONF) + self.assertIn('log /dev/log local3 debug', config) + self.assertIn('log /dev/log local4 info', config) + + # Test service logging options + service_path = base_path + ['service', 'https_front'] + self.cli_set(service_path + ['logging', 'facility', 'local5', 'level', 'notice']) + self.cli_set(service_path + ['logging', 'facility', 'local6', 'level', 'crit']) + self.cli_commit() + + # Test service logging parameters are generated in configuration file + config = read_file(HAPROXY_CONF) + self.assertIn('log /dev/log local5 notice', config) + self.assertIn('log /dev/log local6 crit', config) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py b/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py deleted file mode 100755 index 34f77b95d..000000000 --- a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py +++ /dev/null @@ -1,502 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2023 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 . - -import unittest - -from base_vyostest_shim import VyOSUnitTestSHIM - -from vyos.configsession import ConfigSessionError -from vyos.utils.process import process_named_running -from vyos.utils.file import read_file - -PROCESS_NAME = 'haproxy' -HAPROXY_CONF = '/run/haproxy/haproxy.cfg' -base_path = ['load-balancing', 'reverse-proxy'] -proxy_interface = 'eth1' - -valid_ca_cert = """ -MIIDnTCCAoWgAwIBAgIUewSDtLiZbhg1YEslMnqRl1shoPcwDQYJKoZIhvcNAQEL -BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM -CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0y -NDA0MDEwNTQ3MzJaFw0yOTAzMzEwNTQ3MzJaMFcxCzAJBgNVBAYTAkdCMRMwEQYD -VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5 -T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQC/D6W27rfpdPIf16JHs8fx/7VehyCk8m03dPAQqv6wQiHF5xhXaFZER1+c -nf7oExp9zi/4HJ/KRbcc1loVArXtV0zwAUftBmUeezGVfxhCHKhP89GnV4NB97jj -klHFSxjEoT/0YvJQ1IV/3Cos1T5O8x14WIi31l7WQGYAyWxUXiP8QxGVmF3odEJo -O3e7Ew9HFkamvuL6Z6c4uAVMM7uYXme7q0OM49Wu7C9hj39ZKbjG5FFKZTj+zDKg -SbOiQaFk3blOky/e3ifNjZelGtussYPOMBkUirLvrSGGy7s3lm8Yp5PH5+UkVQB2 -rZyxRdZTC9kh+dShR1s/qcPnDw7lAgMBAAGjYTBfMA8GA1UdEwEB/wQFMAMBAf8w -DgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAd -BgNVHQ4EFgQU/HE2UPn8JQB/9EL52GquPxZqr5MwDQYJKoZIhvcNAQELBQADggEB -AIkMmqyoMqidTa3lvUPJNl4H+Ef/yPQkTkrsOd3WL8DQysyUdMLdQozr3K1bH5XB -wRxoXX211nu4WhN18LsFJRCuHBSxmaNkBGFyl+JNvhPUSI6j0somNMCS75KJ0ZDx -2HZsXmmJFF902VQxCR7vCIrFDrKDYq1e7GQbFS8t46FlpqivQMQWNPt18Bthj/1Y -lO2GKRWFCX8VlOW7FtDQ6B3oC1oAGHBBGogAx7/0gh9DnYBKT14V/kuWW3RNABZJ -ewHO1C6icQdnjtaREDyTP4oyL+uyAfXrFfbpti2hc00f8oYPQZYxj1yxl4UAdNij -mS6YqH/WRioGMe3tBVeSdoo= -""" - -valid_ca_private_key = """ -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/D6W27rfpdPIf -16JHs8fx/7VehyCk8m03dPAQqv6wQiHF5xhXaFZER1+cnf7oExp9zi/4HJ/KRbcc -1loVArXtV0zwAUftBmUeezGVfxhCHKhP89GnV4NB97jjklHFSxjEoT/0YvJQ1IV/ -3Cos1T5O8x14WIi31l7WQGYAyWxUXiP8QxGVmF3odEJoO3e7Ew9HFkamvuL6Z6c4 -uAVMM7uYXme7q0OM49Wu7C9hj39ZKbjG5FFKZTj+zDKgSbOiQaFk3blOky/e3ifN -jZelGtussYPOMBkUirLvrSGGy7s3lm8Yp5PH5+UkVQB2rZyxRdZTC9kh+dShR1s/ -qcPnDw7lAgMBAAECggEAGm+j0kf9koPn7Jf9kEZD6CwlgEraLXiNvBqmDOhcDS9Z -VPTA3XdGWHQ3uofx+VKLW9TntkDfqzEyQP83v6h8W7a0opDKzvUPkMQi/Dh1ttAY -SdfGrozhUINiRbq9LbtSVgKpwrreJGkDf8mK3GE1Gd9xuHEnmahDvwlyE7HLF3Eh -2xJDSAPx3OxcjR5hW7vbojhVCyCfuYTlZB86f0Sb8SqxZMt/y2zKmbzoTqpUBWbg -lBnE7GJoNR07DWjxvEP8r6kQMh670I01SUR42CSK8X8asHhhZHUcggsNno+BBc6K -sy4HzDIYIay6oy0atcVzKsGrlNCveeAiSEcw7x2yAQKBgQDsXz2FbhXYV5Vbt4wU -5EWOa7if/+FG+TcVezOF3xlNBgykjXHQaYTYHrJq0qsEFrNT3ZGm9ezY4LdF3BTt -5z/+i8QlCCw/nr3N7JZx6U5+OJl1j3NLFoFx3+DXo31pgJJEQCHHwdCkF5IuOcZ/ -b3nXkRZ80BVv7XD6F9bMHEwLYQKBgQDO7THcRDbsE6/+7VsTDf0P/JENba3DBBu1 -gjb1ItL5FHJwMgnkUadRZRo0QKye848ugribed39qSoJfNaBJrAT5T8S/9q+lXft -vXUckcBO1CKNaP9gqF5fPIdNHf64GbmCiiHjOTE3rwJjkxJPpzLXyvgBO4aLeesK -ThBdW+iWBQKBgD3crz08knsMcQqP/xl4pLuhdbBqR4tLrh7xH4rp2LVP3/8xBZiG -BT6Kyicq+5cWWdiZJIWN127rYQvnjZK18wmriqomeW4tHX/Ha5hkdyaRqZga8xGz -0iz7at0E7M2v2JgEMNMW5oQLpzZx6IFxq3G/hyMjUnj4q5jIpG7G+SABAoGBAKgT -8Ika+4WcpDssrup2VVTT8Tp4GUkroBo6D8vkInvhiObrLi+/x2mM9tD0q4JdEbNU -yQC454EwFA4q0c2MED/I2QfkvNhLbmO0nVi8ZvlgxEQawjzP5f/zmW8haxI9Cvsm -mkoH3Zt+UzFwd9ItXFX97p6JrErEmA8Bw7chfXXFAoGACWR/c+s7hnX6gzyah3N1 -Db0xAaS6M9fzogcg2OM1i/6OCOcp4Sh1fmPG7tN45CCnFkhgVoRkSSA5MJAe2I/r -xFm72VX7567T+4qIFua2iDxIBA/Z4zmj+RYfhHGPYZjdSjprKJxY6QOv5aoluBvE -mlLy1Hmcry+ukWZtWezZfGY= -""" - -valid_cert = """ -MIIDsTCCApmgAwIBAgIUDKOfYIwwtjww0vAMvJnXnGLhL+0wDQYJKoZIhvcNAQEL -BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM -CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0y -NDA0MDEwNTQ5NTdaFw0yNTA0MDEwNTQ5NTdaMFcxCzAJBgNVBAYTAkdCMRMwEQYD -VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5 -T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQCHtW25Umt6rqm2gfzqAZg1/VsqefZwAqIUAm2T3VwHQZ/2tNdr8ROWASii -W5PToC7N8StMwFl2YoIof+MXGMO00toTTJePZOJKjF9U9hL3kuYuY1+yng4fl+E0 -96xVobb2KY4lMZ2rVwmpB7jkNO2LWxbJ6vHKcwMOhlx/8NEKIoVmkBT1Zkgy5dgn -PgTtJcdVIU75XhQWqBmAUsMmACuZfqSYJbAv3hHz5V+Ejt0dI6mlGM7TXsCC9tKM -64paIKZooFm78IsxJ26jHpZ8eh+SDBz0VBydBFWXm8VhOJ8NlZ1opAh3AWxFZDGt -49uOsy82VmUcHPyoZ8DKYkBFHfSpAgMBAAGjdTBzMAwGA1UdEwEB/wQCMAAwDgYD -VR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBTeTcgM -pRxAMjVBirjzo2QUu5H5fzAfBgNVHSMEGDAWgBT8cTZQ+fwlAH/0QvnYaq4/Fmqv -kzANBgkqhkiG9w0BAQsFAAOCAQEAi4dBcH7TIYwWRW6bWRubMA7ztonV4EYb15Zf -9yNafMWAEEBOii/DFo+j/ky9oInl7ZHw7gTIyXfLEarX/bM6fHOgiyj4zp3u6RnH -5qlBypu/YCnyPjE/GvV05m2rrXnxZ4rCtcoO4u/HyGbV+jGnCmjShKICKyu1FdMd -eeZRrLKPO/yghadGH34WVQnrbaorwlbi+NjB6fxmZQx5HE/SyK/9sb6WCpLMGHoy -MpdQo3lV1ewtL3ElIWDq6mO030Mo5pwpjIU+8yHHNBVzg6mlGVgQPAp0gbUei9aP -CJ8SLmMEi3NDk0E/sPgVC17e6bf2bx2nRuXROZekG2dd90Iu8g== -""" - -valid_cert_private_key = """ -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCHtW25Umt6rqm2 -gfzqAZg1/VsqefZwAqIUAm2T3VwHQZ/2tNdr8ROWASiiW5PToC7N8StMwFl2YoIo -f+MXGMO00toTTJePZOJKjF9U9hL3kuYuY1+yng4fl+E096xVobb2KY4lMZ2rVwmp -B7jkNO2LWxbJ6vHKcwMOhlx/8NEKIoVmkBT1Zkgy5dgnPgTtJcdVIU75XhQWqBmA -UsMmACuZfqSYJbAv3hHz5V+Ejt0dI6mlGM7TXsCC9tKM64paIKZooFm78IsxJ26j -HpZ8eh+SDBz0VBydBFWXm8VhOJ8NlZ1opAh3AWxFZDGt49uOsy82VmUcHPyoZ8DK -YkBFHfSpAgMBAAECggEABofhw0W/ACEMcAjmpNTFkFCUXPGQXWDVD7EzuIZSNdOv -yOm4Rbys6H6/B7wwO6KVagoBf1Cw5Xh1YtFPuoZxsZ+liMD6eLc+SB/j/RTYAhPO -0bvsyK3gSF8w4nGKWLce9M74ZRwThkG6qGijmlDdPyP3r2kn8GoTQzVOWYZbavk/ -H3uE6PsZSWjOY+Mnm3vEmeItPYKGZ5+IP+YiTqZ4NCggBwH7csnR3/kbwY5Ns7jl -3Av+EAdIeUwDNeMfLTzN7GphJR7gL6YQIhGKxE+W0GHXL2FubnnrFx8G75HFh1ay -GkJXEqY5Lbd+7VPS0KcQdwhMSSoJsY5GUORUqrU80QKBgQC/0wJSu+Gfe7dONIby -mnGRppSRIQVRjCjbVIN+Y2h1Kp3aK0qDpV7KFLCiUUtz9rWHR/NB4cDaIW543T55 -/jXUMD2j3EqtbtlsVQfDLQV7DyDrMmBAs4REHmyZmWTzHjCDUO79ahdOlZs34Alz -wfpX3L3WVYGIAJKZtsUZ8FbrGQKBgQC1HFgVZ1PqP9/pW50RMh06BbQrhWPGiWgH -Rn5bFthLkp3uqr9bReBq9tu3sqJuAhFudH68wup+Z+fTcHAcNg2Rs+Q+IKnULdB/ -UQHYoPjeWOvHAuOmgn9iD9OD7GCIv8fZmLit09vAsOWq+NKNBKCknGM70CDrvAlQ -lOAUa34YEQKBgQC5i8GThWiYe3Kzktt1jy6LVDYgq3AZkRl0Diui9UT1EGPfxEAv -VqZ5kcnJOBlj8h9k25PRBi0k0XGqN1dXaS1oMcFt3ofdenuU7iqz/7htcBTHa9Lu -wrYNreAeMuISyADlBEQnm5cvzEZ3pZ1++wLMOhjmWY8Rnnwvczrz/CYXAQKBgH+t -vcNJFvWblkUzWuWWiNgw0TWlUhPTJs2KOuYIku+kK0bohQLZnj6KTZeRjcU0HAnc -gsScPShkJCEBsWeSC7reMVhDOrbknYpEF6MayJgn5ABm3wqyEQ+WzKzCZcPCQCf8 -7KVPKCsOCrufsv/LdVzXC3ZNYggOhhqS+e4rYbehAoGBAIsq252o3vgrunzS5FZx -IONA2FvYrxVbDn5aF8WfNSdKFy3CAlt0P+Fm8gYbrKylIfMXpL8Oqc9RJou5onZP -ZXLrtgVJR9W020qTurO2f91qfU8646n11hR9ObBB1IYbagOU0Pw1Nrq/FRp/u2tx -7i7xFz2WEiQeSCPaKYOiqM3t -""" - - -class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): - def tearDown(self): - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - - self.cli_delete(['interfaces', 'ethernet', proxy_interface, 'address']) - self.cli_delete(base_path) - self.cli_delete(['pki']) - self.cli_commit() - - # Process must be terminated after deleting the config - self.assertFalse(process_named_running(PROCESS_NAME)) - - def base_config(self): - self.cli_set(base_path + ['service', 'https_front', 'mode', 'http']) - self.cli_set(base_path + ['service', 'https_front', 'port', '4433']) - self.cli_set(base_path + ['service', 'https_front', 'backend', 'bk-01']) - - self.cli_set(base_path + ['backend', 'bk-01', 'mode', 'http']) - self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'address', '192.0.2.11']) - self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'port', '9090']) - self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'send-proxy']) - - self.cli_set(base_path + ['global-parameters', 'max-connections', '1000']) - - def configure_pki(self): - - # Valid CA - self.cli_set(['pki', 'ca', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')]) - self.cli_set(['pki', 'ca', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')]) - - # Valid cert - self.cli_set(['pki', 'certificate', 'smoketest', 'certificate', valid_cert.replace('\n','')]) - self.cli_set(['pki', 'certificate', 'smoketest', 'private', 'key', valid_cert_private_key.replace('\n','')]) - - def test_01_lb_reverse_proxy_domain(self): - domains_bk_first = ['n1.example.com', 'n2.example.com', 'n3.example.com'] - domain_bk_second = 'n5.example.com' - frontend = 'https_front' - front_port = '4433' - bk_server_first = '192.0.2.11' - bk_server_second = '192.0.2.12' - bk_first_name = 'bk-01' - bk_second_name = 'bk-02' - bk_server_port = '9090' - mode = 'http' - rule_ten = '10' - rule_twenty = '20' - rule_thirty = '30' - send_proxy = 'send-proxy' - max_connections = '1000' - - back_base = base_path + ['backend'] - - self.cli_set(base_path + ['service', frontend, 'mode', mode]) - self.cli_set(base_path + ['service', frontend, 'port', front_port]) - for domain in domains_bk_first: - self.cli_set(base_path + ['service', frontend, 'rule', rule_ten, 'domain-name', domain]) - self.cli_set(base_path + ['service', frontend, 'rule', rule_ten, 'set', 'backend', bk_first_name]) - self.cli_set(base_path + ['service', frontend, 'rule', rule_twenty, 'domain-name', domain_bk_second]) - self.cli_set(base_path + ['service', frontend, 'rule', rule_twenty, 'set', 'backend', bk_second_name]) - self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'url-path', 'end', '/test']) - self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'set', 'backend', bk_second_name]) - - self.cli_set(back_base + [bk_first_name, 'mode', mode]) - self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, 'address', bk_server_first]) - self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, 'port', bk_server_port]) - self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, send_proxy]) - - self.cli_set(back_base + [bk_second_name, 'mode', mode]) - self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'address', bk_server_second]) - self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'port', bk_server_port]) - self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'backup']) - - self.cli_set(base_path + ['global-parameters', 'max-connections', max_connections]) - - # commit changes - self.cli_commit() - - config = read_file(HAPROXY_CONF) - - # Global - self.assertIn(f'maxconn {max_connections}', config) - - # Frontend - self.assertIn(f'frontend {frontend}', config) - self.assertIn(f'bind [::]:{front_port} v4v6', config) - self.assertIn(f'mode {mode}', config) - for domain in domains_bk_first: - self.assertIn(f'acl {rule_ten} hdr(host) -i {domain}', config) - self.assertIn(f'use_backend {bk_first_name} if {rule_ten}', config) - self.assertIn(f'acl {rule_twenty} hdr(host) -i {domain_bk_second}', config) - self.assertIn(f'use_backend {bk_second_name} if {rule_twenty}', config) - self.assertIn(f'acl {rule_thirty} path -i -m end /test', config) - self.assertIn(f'use_backend {bk_second_name} if {rule_thirty}', config) - - # Backend - self.assertIn(f'backend {bk_first_name}', config) - self.assertIn(f'balance roundrobin', config) - self.assertIn(f'option forwardfor', config) - self.assertIn('http-request add-header X-Forwarded-Proto https if { ssl_fc }', config) - self.assertIn(f'mode {mode}', config) - self.assertIn(f'server {bk_first_name} {bk_server_first}:{bk_server_port} send-proxy', config) - - self.assertIn(f'backend {bk_second_name}', config) - self.assertIn(f'mode {mode}', config) - self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port}', config) - self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port} backup', config) - - def test_02_lb_reverse_proxy_cert_not_exists(self): - self.base_config() - self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert']) - - with self.assertRaises(ConfigSessionError) as e: - self.cli_commit() - # self.assertIn('\nCertificates does not exist in PKI\n', str(e.exception)) - - self.cli_delete(base_path) - self.configure_pki() - - self.base_config() - self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert']) - - with self.assertRaises(ConfigSessionError) as e: - self.cli_commit() - # self.assertIn('\nCertificate "cert" does not exist\n', str(e.exception)) - - self.cli_delete(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert']) - self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'smoketest']) - self.cli_commit() - - def test_03_lb_reverse_proxy_ca_not_exists(self): - self.base_config() - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test']) - - with self.assertRaises(ConfigSessionError) as e: - self.cli_commit() - # self.assertIn('\nCA certificates does not exist in PKI\n', str(e.exception)) - - self.cli_delete(base_path) - self.configure_pki() - - self.base_config() - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test']) - - with self.assertRaises(ConfigSessionError) as e: - self.cli_commit() - # self.assertIn('\nCA certificate "ca-test" does not exist\n', str(e.exception)) - - self.cli_delete(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test']) - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest']) - self.cli_commit() - - def test_04_lb_reverse_proxy_backend_ssl_no_verify(self): - # Setup base - self.configure_pki() - self.base_config() - - # Set no-verify option - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'no-verify']) - self.cli_commit() - - # Test no-verify option - config = read_file(HAPROXY_CONF) - self.assertIn('server bk-01 192.0.2.11:9090 send-proxy ssl verify none', config) - - # Test setting ca-certificate alongside no-verify option fails, to test config validation - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest']) - with self.assertRaises(ConfigSessionError) as e: - self.cli_commit() - - def test_05_lb_reverse_proxy_backend_http_check(self): - # Setup base - self.base_config() - - # Set http-check - self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'method', 'get']) - self.cli_commit() - - # Test http-check - config = read_file(HAPROXY_CONF) - self.assertIn('option httpchk', config) - self.assertIn('http-check send meth GET', config) - - # Set http-check with uri and status - self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'uri', '/health']) - self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200']) - self.cli_commit() - - # Test http-check with uri and status - config = read_file(HAPROXY_CONF) - self.assertIn('option httpchk', config) - self.assertIn('http-check send meth GET uri /health', config) - self.assertIn('http-check expect status 200', config) - - # Set http-check with string - self.cli_delete(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200']) - self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'string', 'success']) - self.cli_commit() - - # Test http-check with string - config = read_file(HAPROXY_CONF) - self.assertIn('option httpchk', config) - self.assertIn('http-check send meth GET uri /health', config) - self.assertIn('http-check expect string success', config) - - # Test configuring both http-check & health-check fails validation script - self.cli_set(base_path + ['backend', 'bk-01', 'health-check', 'ldap']) - with self.assertRaises(ConfigSessionError) as e: - self.cli_commit() - - def test_06_lb_reverse_proxy_tcp_mode(self): - frontend = 'tcp_8443' - mode = 'tcp' - front_port = '8433' - tcp_request_delay = "5000" - rule_thirty = '30' - domain_bk = 'n6.example.com' - ssl_opt = "req-ssl-sni" - bk_name = 'bk-03' - bk_server = '192.0.2.11' - bk_server_port = '9090' - - back_base = base_path + ['backend'] - - self.cli_set(base_path + ['service', frontend, 'mode', mode]) - self.cli_set(base_path + ['service', frontend, 'port', front_port]) - self.cli_set(base_path + ['service', frontend, 'tcp-request', 'inspect-delay', tcp_request_delay]) - - self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'domain-name', domain_bk]) - self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'ssl', ssl_opt]) - self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'set', 'backend', bk_name]) - - self.cli_set(back_base + [bk_name, 'mode', mode]) - self.cli_set(back_base + [bk_name, 'server', bk_name, 'address', bk_server]) - self.cli_set(back_base + [bk_name, 'server', bk_name, 'port', bk_server_port]) - - # commit changes - self.cli_commit() - - config = read_file(HAPROXY_CONF) - - # Frontend - self.assertIn(f'frontend {frontend}', config) - self.assertIn(f'bind [::]:{front_port} v4v6', config) - self.assertIn(f'mode {mode}', config) - - self.assertIn(f'tcp-request inspect-delay {tcp_request_delay}', config) - self.assertIn(f"tcp-request content accept if {{ req_ssl_hello_type 1 }}", config) - self.assertIn(f'acl {rule_thirty} req_ssl_sni -i {domain_bk}', config) - self.assertIn(f'use_backend {bk_name} if {rule_thirty}', config) - - # Backend - self.assertIn(f'backend {bk_name}', config) - self.assertIn(f'balance roundrobin', config) - self.assertIn(f'mode {mode}', config) - self.assertIn(f'server {bk_name} {bk_server}:{bk_server_port}', config) - - def test_07_lb_reverse_proxy_http_response_headers(self): - # Setup base - self.configure_pki() - self.base_config() - - # Set example headers in both frontend and backend - self.cli_set(base_path + ['service', 'https_front', 'http-response-headers', 'Cache-Control', 'value', 'max-age=604800']) - self.cli_set(base_path + ['backend', 'bk-01', 'http-response-headers', 'Proxy-Backend-ID', 'value', 'bk-01']) - self.cli_commit() - - # Test headers are present in generated configuration file - config = read_file(HAPROXY_CONF) - self.assertIn('http-response set-header Cache-Control \'max-age=604800\'', config) - self.assertIn('http-response set-header Proxy-Backend-ID \'bk-01\'', config) - - # Test setting alongside modes other than http is blocked by validation conditions - self.cli_set(base_path + ['service', 'https_front', 'mode', 'tcp']) - with self.assertRaises(ConfigSessionError) as e: - self.cli_commit() - - def test_08_lb_reverse_proxy_tcp_health_checks(self): - # Setup PKI - self.configure_pki() - - # Define variables - frontend = 'fe_ldaps' - mode = 'tcp' - health_check = 'ldap' - front_port = '636' - bk_name = 'bk_ldap' - bk_servers = ['192.0.2.11', '192.0.2.12'] - bk_server_port = '389' - - # Configure frontend - self.cli_set(base_path + ['service', frontend, 'mode', mode]) - self.cli_set(base_path + ['service', frontend, 'port', front_port]) - self.cli_set(base_path + ['service', frontend, 'ssl', 'certificate', 'smoketest']) - - # Configure backend - self.cli_set(base_path + ['backend', bk_name, 'mode', mode]) - self.cli_set(base_path + ['backend', bk_name, 'health-check', health_check]) - for index, bk_server in enumerate(bk_servers): - self.cli_set(base_path + ['backend', bk_name, 'server', f'srv-{index}', 'address', bk_server]) - self.cli_set(base_path + ['backend', bk_name, 'server', f'srv-{index}', 'port', bk_server_port]) - - # Commit & read config - self.cli_commit() - config = read_file(HAPROXY_CONF) - - # Validate Frontend - self.assertIn(f'frontend {frontend}', config) - self.assertIn(f'bind [::]:{front_port} v4v6 ssl crt /run/haproxy/smoketest.pem', config) - self.assertIn(f'mode {mode}', config) - self.assertIn(f'backend {bk_name}', config) - - # Validate Backend - self.assertIn(f'backend {bk_name}', config) - self.assertIn(f'option {health_check}-check', config) - self.assertIn(f'mode {mode}', config) - for index, bk_server in enumerate(bk_servers): - self.assertIn(f'server srv-{index} {bk_server}:{bk_server_port}', config) - - # Validate SMTP option renders correctly - self.cli_set(base_path + ['backend', bk_name, 'health-check', 'smtp']) - self.cli_commit() - config = read_file(HAPROXY_CONF) - self.assertIn(f'option smtpchk', config) - - def test_09_lb_reverse_proxy_logging(self): - # Setup base - self.base_config() - self.cli_commit() - - # Ensure default logging configuration is present - config = read_file(HAPROXY_CONF) - - # Test global-parameters logging options - self.cli_set(base_path + ['global-parameters', 'logging', 'facility', 'local1', 'level', 'err']) - self.cli_set(base_path + ['global-parameters', 'logging', 'facility', 'local2', 'level', 'warning']) - self.cli_commit() - - # Test global logging parameters are generated in configuration file - config = read_file(HAPROXY_CONF) - self.assertIn('log /dev/log local1 err', config) - self.assertIn('log /dev/log local2 warning', config) - - # Test backend logging options - backend_path = base_path + ['backend', 'bk-01'] - self.cli_set(backend_path + ['logging', 'facility', 'local3', 'level', 'debug']) - self.cli_set(backend_path + ['logging', 'facility', 'local4', 'level', 'info']) - self.cli_commit() - - # Test backend logging parameters are generated in configuration file - config = read_file(HAPROXY_CONF) - self.assertIn('log /dev/log local3 debug', config) - self.assertIn('log /dev/log local4 info', config) - - # Test service logging options - service_path = base_path + ['service', 'https_front'] - self.cli_set(service_path + ['logging', 'facility', 'local5', 'level', 'notice']) - self.cli_set(service_path + ['logging', 'facility', 'local6', 'level', 'crit']) - self.cli_commit() - - # Test service logging parameters are generated in configuration file - config = read_file(HAPROXY_CONF) - self.assertIn('log /dev/log local5 notice', config) - self.assertIn('log /dev/log local6 crit', config) - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/src/conf_mode/load-balancing_haproxy.py b/src/conf_mode/load-balancing_haproxy.py new file mode 100644 index 000000000..45042dd52 --- /dev/null +++ b/src/conf_mode/load-balancing_haproxy.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2024 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 . + +import os + +from sys import exit +from shutil import rmtree + +from vyos.config import Config +from vyos.configverify import verify_pki_certificate +from vyos.configverify import verify_pki_ca_certificate +from vyos.utils.dict import dict_search +from vyos.utils.process import call +from vyos.utils.network import check_port_availability +from vyos.utils.network import is_listen_port_bind_service +from vyos.pki import find_chain +from vyos.pki import load_certificate +from vyos.pki import load_private_key +from vyos.pki import encode_certificate +from vyos.pki import encode_private_key +from vyos.template import render +from vyos.utils.file import write_file +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +load_balancing_dir = '/run/haproxy' +load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg' +systemd_service = 'haproxy.service' +systemd_override = '/run/systemd/system/haproxy.service.d/10-override.conf' + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['load-balancing', 'haproxy'] + if not conf.exists(base): + return None + lb = conf.get_config_dict(base, + get_first_key=True, + key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + with_recursive_defaults=True, + with_pki=True) + + return lb + +def verify(lb): + if not lb: + return None + + if 'backend' not in lb or 'service' not in lb: + raise ConfigError(f'"service" and "backend" must be configured!') + + for front, front_config in lb['service'].items(): + if 'port' not in front_config: + raise ConfigError(f'"{front} service port" must be configured!') + + # Check if bind address:port are used by another service + tmp_address = front_config.get('address', '0.0.0.0') + tmp_port = front_config['port'] + if check_port_availability(tmp_address, int(tmp_port), 'tcp') is not True and \ + not is_listen_port_bind_service(int(tmp_port), 'haproxy'): + raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service') + + for back, back_config in lb['backend'].items(): + if 'http_check' in back_config: + http_check = back_config['http_check'] + if 'expect' in http_check and 'status' in http_check['expect'] and 'string' in http_check['expect']: + raise ConfigError(f'"expect status" and "expect string" can not be configured together!') + + if 'health_check' in back_config: + if back_config['mode'] != 'tcp': + raise ConfigError(f'backend "{back}" can only be configured with {back_config["health_check"]} ' + + f'health-check whilst in TCP mode!') + if 'http_check' in back_config: + raise ConfigError(f'backend "{back}" cannot be configured with both http-check and health-check!') + + if 'server' not in back_config: + raise ConfigError(f'"{back} server" must be configured!') + + for bk_server, bk_server_conf in back_config['server'].items(): + if 'address' not in bk_server_conf or 'port' not in bk_server_conf: + raise ConfigError(f'"backend {back} server {bk_server} address and port" must be configured!') + + if {'send_proxy', 'send_proxy_v2'} <= set(bk_server_conf): + raise ConfigError(f'Cannot use both "send-proxy" and "send-proxy-v2" for server "{bk_server}"') + + if 'ssl' in back_config: + if {'no_verify', 'ca_certificate'} <= set(back_config['ssl']): + raise ConfigError(f'backend {back} cannot have both ssl options no-verify and ca-certificate set!') + + # Check if http-response-headers are configured in any frontend/backend where mode != http + for group in ['service', 'backend']: + for config_name, config in lb[group].items(): + if 'http_response_headers' in config and config['mode'] != 'http': + raise ConfigError(f'{group} {config_name} must be set to http mode to use http_response_headers!') + + for front, front_config in lb['service'].items(): + for cert in dict_search('ssl.certificate', front_config) or []: + verify_pki_certificate(lb, cert) + + for back, back_config in lb['backend'].items(): + tmp = dict_search('ssl.ca_certificate', back_config) + if tmp: verify_pki_ca_certificate(lb, tmp) + + +def generate(lb): + if not lb: + # Delete /run/haproxy/haproxy.cfg + config_files = [load_balancing_conf_file, systemd_override] + for file in config_files: + if os.path.isfile(file): + os.unlink(file) + # Delete old directories + if os.path.isdir(load_balancing_dir): + rmtree(load_balancing_dir, ignore_errors=True) + + return None + + # Create load-balance dir + if not os.path.isdir(load_balancing_dir): + os.mkdir(load_balancing_dir) + + loaded_ca_certs = {load_certificate(c['certificate']) + for c in lb['pki']['ca'].values()} if 'ca' in lb['pki'] else {} + + # SSL Certificates for frontend + for front, front_config in lb['service'].items(): + if 'ssl' not in front_config: + continue + + if 'certificate' in front_config['ssl']: + cert_names = front_config['ssl']['certificate'] + + for cert_name in cert_names: + pki_cert = lb['pki']['certificate'][cert_name] + cert_file_path = os.path.join(load_balancing_dir, f'{cert_name}.pem') + cert_key_path = os.path.join(load_balancing_dir, f'{cert_name}.pem.key') + + loaded_pki_cert = load_certificate(pki_cert['certificate']) + cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs) + + write_file(cert_file_path, + '\n'.join(encode_certificate(c) for c in cert_full_chain)) + + if 'private' in pki_cert and 'key' in pki_cert['private']: + loaded_key = load_private_key(pki_cert['private']['key'], passphrase=None, wrap_tags=True) + key_pem = encode_private_key(loaded_key, passphrase=None) + write_file(cert_key_path, key_pem) + + # SSL Certificates for backend + for back, back_config in lb['backend'].items(): + if 'ssl' not in back_config: + continue + + if 'ca_certificate' in back_config['ssl']: + ca_name = back_config['ssl']['ca_certificate'] + ca_cert_file_path = os.path.join(load_balancing_dir, f'{ca_name}.pem') + ca_chains = [] + + pki_ca_cert = lb['pki']['ca'][ca_name] + loaded_ca_cert = load_certificate(pki_ca_cert['certificate']) + ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs) + ca_chains.append('\n'.join(encode_certificate(c) for c in ca_full_chain)) + write_file(ca_cert_file_path, '\n'.join(ca_chains)) + + render(load_balancing_conf_file, 'load-balancing/haproxy.cfg.j2', lb) + render(systemd_override, 'load-balancing/override_haproxy.conf.j2', lb) + + return None + +def apply(lb): + call('systemctl daemon-reload') + if not lb: + call(f'systemctl stop {systemd_service}') + else: + call(f'systemctl reload-or-restart {systemd_service}') + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_reverse-proxy.py deleted file mode 100755 index 17226efe9..000000000 --- a/src/conf_mode/load-balancing_reverse-proxy.py +++ /dev/null @@ -1,206 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2023-2024 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 . - -import os - -from sys import exit -from shutil import rmtree - -from vyos.config import Config -from vyos.configverify import verify_pki_certificate -from vyos.configverify import verify_pki_ca_certificate -from vyos.utils.dict import dict_search -from vyos.utils.process import call -from vyos.utils.network import check_port_availability -from vyos.utils.network import is_listen_port_bind_service -from vyos.pki import find_chain -from vyos.pki import load_certificate -from vyos.pki import load_private_key -from vyos.pki import encode_certificate -from vyos.pki import encode_private_key -from vyos.template import render -from vyos.utils.file import write_file -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -load_balancing_dir = '/run/haproxy' -load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg' -systemd_service = 'haproxy.service' -systemd_override = '/run/systemd/system/haproxy.service.d/10-override.conf' - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - base = ['load-balancing', 'reverse-proxy'] - if not conf.exists(base): - return None - lb = conf.get_config_dict(base, - get_first_key=True, - key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - with_recursive_defaults=True, - with_pki=True) - - return lb - -def verify(lb): - if not lb: - return None - - if 'backend' not in lb or 'service' not in lb: - raise ConfigError(f'"service" and "backend" must be configured!') - - for front, front_config in lb['service'].items(): - if 'port' not in front_config: - raise ConfigError(f'"{front} service port" must be configured!') - - # Check if bind address:port are used by another service - tmp_address = front_config.get('address', '0.0.0.0') - tmp_port = front_config['port'] - if check_port_availability(tmp_address, int(tmp_port), 'tcp') is not True and \ - not is_listen_port_bind_service(int(tmp_port), 'haproxy'): - raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service') - - for back, back_config in lb['backend'].items(): - if 'http_check' in back_config: - http_check = back_config['http_check'] - if 'expect' in http_check and 'status' in http_check['expect'] and 'string' in http_check['expect']: - raise ConfigError(f'"expect status" and "expect string" can not be configured together!') - - if 'health_check' in back_config: - if back_config['mode'] != 'tcp': - raise ConfigError(f'backend "{back}" can only be configured with {back_config["health_check"]} ' + - f'health-check whilst in TCP mode!') - if 'http_check' in back_config: - raise ConfigError(f'backend "{back}" cannot be configured with both http-check and health-check!') - - if 'server' not in back_config: - raise ConfigError(f'"{back} server" must be configured!') - - for bk_server, bk_server_conf in back_config['server'].items(): - if 'address' not in bk_server_conf or 'port' not in bk_server_conf: - raise ConfigError(f'"backend {back} server {bk_server} address and port" must be configured!') - - if {'send_proxy', 'send_proxy_v2'} <= set(bk_server_conf): - raise ConfigError(f'Cannot use both "send-proxy" and "send-proxy-v2" for server "{bk_server}"') - - if 'ssl' in back_config: - if {'no_verify', 'ca_certificate'} <= set(back_config['ssl']): - raise ConfigError(f'backend {back} cannot have both ssl options no-verify and ca-certificate set!') - - # Check if http-response-headers are configured in any frontend/backend where mode != http - for group in ['service', 'backend']: - for config_name, config in lb[group].items(): - if 'http_response_headers' in config and config['mode'] != 'http': - raise ConfigError(f'{group} {config_name} must be set to http mode to use http_response_headers!') - - for front, front_config in lb['service'].items(): - for cert in dict_search('ssl.certificate', front_config) or []: - verify_pki_certificate(lb, cert) - - for back, back_config in lb['backend'].items(): - tmp = dict_search('ssl.ca_certificate', back_config) - if tmp: verify_pki_ca_certificate(lb, tmp) - - -def generate(lb): - if not lb: - # Delete /run/haproxy/haproxy.cfg - config_files = [load_balancing_conf_file, systemd_override] - for file in config_files: - if os.path.isfile(file): - os.unlink(file) - # Delete old directories - if os.path.isdir(load_balancing_dir): - rmtree(load_balancing_dir, ignore_errors=True) - - return None - - # Create load-balance dir - if not os.path.isdir(load_balancing_dir): - os.mkdir(load_balancing_dir) - - loaded_ca_certs = {load_certificate(c['certificate']) - for c in lb['pki']['ca'].values()} if 'ca' in lb['pki'] else {} - - # SSL Certificates for frontend - for front, front_config in lb['service'].items(): - if 'ssl' not in front_config: - continue - - if 'certificate' in front_config['ssl']: - cert_names = front_config['ssl']['certificate'] - - for cert_name in cert_names: - pki_cert = lb['pki']['certificate'][cert_name] - cert_file_path = os.path.join(load_balancing_dir, f'{cert_name}.pem') - cert_key_path = os.path.join(load_balancing_dir, f'{cert_name}.pem.key') - - loaded_pki_cert = load_certificate(pki_cert['certificate']) - cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs) - - write_file(cert_file_path, - '\n'.join(encode_certificate(c) for c in cert_full_chain)) - - if 'private' in pki_cert and 'key' in pki_cert['private']: - loaded_key = load_private_key(pki_cert['private']['key'], passphrase=None, wrap_tags=True) - key_pem = encode_private_key(loaded_key, passphrase=None) - write_file(cert_key_path, key_pem) - - # SSL Certificates for backend - for back, back_config in lb['backend'].items(): - if 'ssl' not in back_config: - continue - - if 'ca_certificate' in back_config['ssl']: - ca_name = back_config['ssl']['ca_certificate'] - ca_cert_file_path = os.path.join(load_balancing_dir, f'{ca_name}.pem') - ca_chains = [] - - pki_ca_cert = lb['pki']['ca'][ca_name] - loaded_ca_cert = load_certificate(pki_ca_cert['certificate']) - ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs) - ca_chains.append('\n'.join(encode_certificate(c) for c in ca_full_chain)) - write_file(ca_cert_file_path, '\n'.join(ca_chains)) - - render(load_balancing_conf_file, 'load-balancing/haproxy.cfg.j2', lb) - render(systemd_override, 'load-balancing/override_haproxy.conf.j2', lb) - - return None - -def apply(lb): - call('systemctl daemon-reload') - if not lb: - call(f'systemctl stop {systemd_service}') - else: - call(f'systemctl reload-or-restart {systemd_service}') - - return None - - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index 233d73ba8..45e0129a3 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -71,7 +71,7 @@ sync_search = [ }, { 'keys': ['certificate', 'ca_certificate'], - 'path': ['load_balancing', 'reverse_proxy'], + 'path': ['load_balancing', 'haproxy'], }, { 'keys': ['key'], diff --git a/src/migration-scripts/reverse-proxy/1-to-2 b/src/migration-scripts/reverse-proxy/1-to-2 new file mode 100755 index 000000000..61612bc36 --- /dev/null +++ b/src/migration-scripts/reverse-proxy/1-to-2 @@ -0,0 +1,27 @@ +# Copyright 2024 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see . + +# T6745: Rename base node to haproxy + +from vyos.configtree import ConfigTree + +base = ['load-balancing', 'reverse-proxy'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(base): + # Nothing to do + return + + config.rename(base, 'haproxy') diff --git a/src/op_mode/load-balancing_haproxy.py b/src/op_mode/load-balancing_haproxy.py new file mode 100755 index 000000000..ae6734e16 --- /dev/null +++ b/src/op_mode/load-balancing_haproxy.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2024 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 . + +import json +import socket +import sys + +from tabulate import tabulate +from vyos.configquery import ConfigTreeQuery + +import vyos.opmode + +socket_path = '/run/haproxy/admin.sock' +timeout = 5 + + +def _execute_haproxy_command(command): + """Execute a command on the HAProxy UNIX socket and retrieve the response. + + Args: + command (str): The command to be executed. + + Returns: + str: The response received from the HAProxy UNIX socket. + + Raises: + socket.error: If there is an error while connecting or communicating with the socket. + + Finally: + Closes the socket connection. + + Notes: + - HAProxy expects a newline character at the end of the command. + - The socket connection is established using the HAProxy UNIX socket. + - The response from the socket is received and decoded. + + Example: + response = _execute_haproxy_command('show stat') + print(response) + """ + try: + # HAProxy expects new line for command + command = f'{command}\n' + + # Connect to the HAProxy UNIX socket + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(socket_path) + + # Set the socket timeout + sock.settimeout(timeout) + + # Send the command + sock.sendall(command.encode()) + + # Receive and decode the response + response = b'' + while True: + data = sock.recv(4096) + if not data: + break + response += data + response = response.decode() + + return (response) + + except socket.error as e: + print(f"Error: {e}") + + finally: + # Close the socket + sock.close() + + +def _convert_seconds(seconds): + """Convert seconds to days, hours, minutes, and seconds. + + Args: + seconds (int): The number of seconds to convert. + + Returns: + tuple: A tuple containing the number of days, hours, minutes, and seconds. + """ + minutes = seconds // 60 + hours = minutes // 60 + days = hours // 24 + + return days, hours % 24, minutes % 60, seconds % 60 + + +def _last_change_format(seconds): + """Format the time components into a string representation. + + Args: + seconds (int): The total number of seconds. + + Returns: + str: The formatted time string with days, hours, minutes, and seconds. + + Examples: + >>> _last_change_format(1434) + '23m54s' + >>> _last_change_format(93734) + '1d0h23m54s' + >>> _last_change_format(85434) + '23h23m54s' + """ + days, hours, minutes, seconds = _convert_seconds(seconds) + time_format = "" + + if days: + time_format += f"{days}d" + if hours: + time_format += f"{hours}h" + if minutes: + time_format += f"{minutes}m" + if seconds: + time_format += f"{seconds}s" + + return time_format + + +def _get_json_data(): + """Get haproxy data format JSON""" + return _execute_haproxy_command('show stat json') + + +def _get_raw_data(): + """Retrieve raw data from JSON and organize it into a dictionary. + + Returns: + dict: A dictionary containing the organized data categorized + into frontend, backend, and server. + """ + + data = json.loads(_get_json_data()) + lb_dict = {'frontend': [], 'backend': [], 'server': []} + + for key in data: + frontend = [] + backend = [] + server = [] + for entry in key: + obj_type = entry['objType'].lower() + position = entry['field']['pos'] + name = entry['field']['name'] + value = entry['value']['value'] + + dict_entry = {'pos': position, 'name': {name: value}} + + if obj_type == 'frontend': + frontend.append(dict_entry) + elif obj_type == 'backend': + backend.append(dict_entry) + elif obj_type == 'server': + server.append(dict_entry) + + if len(frontend) > 0: + lb_dict['frontend'].append(frontend) + if len(backend) > 0: + lb_dict['backend'].append(backend) + if len(server) > 0: + lb_dict['server'].append(server) + + return lb_dict + + +def _get_formatted_output(data): + """ + Format the data into a tabulated output. + + Args: + data (dict): The data to be formatted. + + Returns: + str: The tabulated output representing the formatted data. + """ + table = [] + headers = [ + "Proxy name", "Role", "Status", "Req rate", "Resp time", "Last change" + ] + + for key in data: + for item in data[key]: + row = [None] * len(headers) + + for element in item: + if 'pxname' in element['name']: + row[0] = element['name']['pxname'] + elif 'svname' in element['name']: + row[1] = element['name']['svname'] + elif 'status' in element['name']: + row[2] = element['name']['status'] + elif 'req_rate' in element['name']: + row[3] = element['name']['req_rate'] + elif 'rtime' in element['name']: + row[4] = f"{element['name']['rtime']} ms" + elif 'lastchg' in element['name']: + row[5] = _last_change_format(element['name']['lastchg']) + table.append(row) + + out = tabulate(table, headers, numalign="left") + return out + + +def show(raw: bool): + config = ConfigTreeQuery() + if not config.exists('load-balancing haproxy'): + raise vyos.opmode.UnconfiguredSubsystem('Haproxy is not configured') + + data = _get_raw_data() + if raw: + return data + else: + return _get_formatted_output(data) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/restart.py b/src/op_mode/restart.py index a83c8b9d8..3b0031f34 100755 --- a/src/op_mode/restart.py +++ b/src/op_mode/restart.py @@ -41,6 +41,10 @@ service_map = { 'systemd_service': 'pdns-recursor', 'path': ['service', 'dns', 'forwarding'], }, + 'haproxy': { + 'systemd_service': 'haproxy', + 'path': ['load-balancing', 'haproxy'], + }, 'igmp_proxy': { 'systemd_service': 'igmpproxy', 'path': ['protocols', 'igmp-proxy'], @@ -53,10 +57,6 @@ service_map = { 'systemd_service': 'avahi-daemon', 'path': ['service', 'mdns', 'repeater'], }, - 'reverse_proxy': { - 'systemd_service': 'haproxy', - 'path': ['load-balancing', 'reverse-proxy'], - }, 'router_advert': { 'systemd_service': 'radvd', 'path': ['service', 'router-advert'], @@ -83,10 +83,10 @@ services = typing.Literal[ 'dhcpv6', 'dns_dynamic', 'dns_forwarding', + 'haproxy', 'igmp_proxy', 'ipsec', 'mdns_repeater', - 'reverse_proxy', 'router_advert', 'snmp', 'ssh', diff --git a/src/op_mode/reverseproxy.py b/src/op_mode/reverseproxy.py deleted file mode 100755 index 19704182a..000000000 --- a/src/op_mode/reverseproxy.py +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2023-2024 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 . - -import json -import socket -import sys - -from tabulate import tabulate -from vyos.configquery import ConfigTreeQuery - -import vyos.opmode - -socket_path = '/run/haproxy/admin.sock' -timeout = 5 - - -def _execute_haproxy_command(command): - """Execute a command on the HAProxy UNIX socket and retrieve the response. - - Args: - command (str): The command to be executed. - - Returns: - str: The response received from the HAProxy UNIX socket. - - Raises: - socket.error: If there is an error while connecting or communicating with the socket. - - Finally: - Closes the socket connection. - - Notes: - - HAProxy expects a newline character at the end of the command. - - The socket connection is established using the HAProxy UNIX socket. - - The response from the socket is received and decoded. - - Example: - response = _execute_haproxy_command('show stat') - print(response) - """ - try: - # HAProxy expects new line for command - command = f'{command}\n' - - # Connect to the HAProxy UNIX socket - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(socket_path) - - # Set the socket timeout - sock.settimeout(timeout) - - # Send the command - sock.sendall(command.encode()) - - # Receive and decode the response - response = b'' - while True: - data = sock.recv(4096) - if not data: - break - response += data - response = response.decode() - - return (response) - - except socket.error as e: - print(f"Error: {e}") - - finally: - # Close the socket - sock.close() - - -def _convert_seconds(seconds): - """Convert seconds to days, hours, minutes, and seconds. - - Args: - seconds (int): The number of seconds to convert. - - Returns: - tuple: A tuple containing the number of days, hours, minutes, and seconds. - """ - minutes = seconds // 60 - hours = minutes // 60 - days = hours // 24 - - return days, hours % 24, minutes % 60, seconds % 60 - - -def _last_change_format(seconds): - """Format the time components into a string representation. - - Args: - seconds (int): The total number of seconds. - - Returns: - str: The formatted time string with days, hours, minutes, and seconds. - - Examples: - >>> _last_change_format(1434) - '23m54s' - >>> _last_change_format(93734) - '1d0h23m54s' - >>> _last_change_format(85434) - '23h23m54s' - """ - days, hours, minutes, seconds = _convert_seconds(seconds) - time_format = "" - - if days: - time_format += f"{days}d" - if hours: - time_format += f"{hours}h" - if minutes: - time_format += f"{minutes}m" - if seconds: - time_format += f"{seconds}s" - - return time_format - - -def _get_json_data(): - """Get haproxy data format JSON""" - return _execute_haproxy_command('show stat json') - - -def _get_raw_data(): - """Retrieve raw data from JSON and organize it into a dictionary. - - Returns: - dict: A dictionary containing the organized data categorized - into frontend, backend, and server. - """ - - data = json.loads(_get_json_data()) - lb_dict = {'frontend': [], 'backend': [], 'server': []} - - for key in data: - frontend = [] - backend = [] - server = [] - for entry in key: - obj_type = entry['objType'].lower() - position = entry['field']['pos'] - name = entry['field']['name'] - value = entry['value']['value'] - - dict_entry = {'pos': position, 'name': {name: value}} - - if obj_type == 'frontend': - frontend.append(dict_entry) - elif obj_type == 'backend': - backend.append(dict_entry) - elif obj_type == 'server': - server.append(dict_entry) - - if len(frontend) > 0: - lb_dict['frontend'].append(frontend) - if len(backend) > 0: - lb_dict['backend'].append(backend) - if len(server) > 0: - lb_dict['server'].append(server) - - return lb_dict - - -def _get_formatted_output(data): - """ - Format the data into a tabulated output. - - Args: - data (dict): The data to be formatted. - - Returns: - str: The tabulated output representing the formatted data. - """ - table = [] - headers = [ - "Proxy name", "Role", "Status", "Req rate", "Resp time", "Last change" - ] - - for key in data: - for item in data[key]: - row = [None] * len(headers) - - for element in item: - if 'pxname' in element['name']: - row[0] = element['name']['pxname'] - elif 'svname' in element['name']: - row[1] = element['name']['svname'] - elif 'status' in element['name']: - row[2] = element['name']['status'] - elif 'req_rate' in element['name']: - row[3] = element['name']['req_rate'] - elif 'rtime' in element['name']: - row[4] = f"{element['name']['rtime']} ms" - elif 'lastchg' in element['name']: - row[5] = _last_change_format(element['name']['lastchg']) - table.append(row) - - out = tabulate(table, headers, numalign="left") - return out - - -def show(raw: bool): - config = ConfigTreeQuery() - if not config.exists('load-balancing reverse-proxy'): - raise vyos.opmode.UnconfiguredSubsystem('Reverse-proxy is not configured') - - data = _get_raw_data() - if raw: - return data - else: - return _get_formatted_output(data) - - -if __name__ == '__main__': - try: - res = vyos.opmode.run(sys.modules[__name__]) - if res: - print(res) - except (ValueError, vyos.opmode.Error) as e: - print(e) - sys.exit(1) -- cgit v1.2.3 From 7765e037b9fc9913b58a39bfa39209ebfd3b3f71 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:00:15 +0200 Subject: haproxy: T6745: Add haproxy migration to config test --- smoketest/configs/basic-vyos | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/smoketest/configs/basic-vyos b/smoketest/configs/basic-vyos index e95d7458f..242f3d1de 100644 --- a/smoketest/configs/basic-vyos +++ b/smoketest/configs/basic-vyos @@ -32,6 +32,27 @@ interfaces { loopback lo { } } +load-balancing { + reverse-proxy { + backend bk-01 { + balance "round-robin" + mode "tcp" + server srv01 { + address "192.0.2.11" + port "8881" + } + server srv02 { + address "192.0.2.12" + port "8882" + } + } + service my-tcp-api { + backend "bk-01" + mode "tcp" + port "8888" + } + } +} protocols { static { arp 192.168.0.20 { -- cgit v1.2.3 From e5d2ac54150922640c08bacab124e96c7bbd1f7f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 11 Oct 2024 08:23:46 +0300 Subject: T6712: Add nonproduction banner (#4149) (cherry picked from commit 3abe7c72c95c3d9b825db08b092c555786e9fbcf) Co-authored-by: Viacheslav Hletenko --- data/templates/login/motd_vyos_nonproduction.j2 | 3 +++ src/conf_mode/system_login_banner.py | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 data/templates/login/motd_vyos_nonproduction.j2 diff --git a/data/templates/login/motd_vyos_nonproduction.j2 b/data/templates/login/motd_vyos_nonproduction.j2 new file mode 100644 index 000000000..32922f27f --- /dev/null +++ b/data/templates/login/motd_vyos_nonproduction.j2 @@ -0,0 +1,3 @@ + +--- +Warning: This VyOS system is not a stable long-term support version and is not intended for production use. diff --git a/src/conf_mode/system_login_banner.py b/src/conf_mode/system_login_banner.py index 923e1bf57..5826d8042 100755 --- a/src/conf_mode/system_login_banner.py +++ b/src/conf_mode/system_login_banner.py @@ -28,6 +28,7 @@ airbag.enable() PRELOGIN_FILE = r'/etc/issue' PRELOGIN_NET_FILE = r'/etc/issue.net' POSTLOGIN_FILE = r'/etc/motd' +POSTLOGIN_VYOS_FILE = r'/run/motd.d/01-vyos-nonproduction' default_config_data = { 'issue': 'Welcome to VyOS - \\n \\l\n\n', @@ -94,6 +95,9 @@ def apply(banner): render(POSTLOGIN_FILE, 'login/default_motd.j2', banner, permission=0o644, user='root', group='root') + render(POSTLOGIN_VYOS_FILE, 'login/motd_vyos_nonproduction.j2', banner, + permission=0o644, user='root', group='root') + return None if __name__ == '__main__': -- cgit v1.2.3 From 3f933f1642debfca5c9e3873790a5742ef242c75 Mon Sep 17 00:00:00 2001 From: Robert Göhler Date: Thu, 17 Oct 2024 08:35:47 +0200 Subject: T973: add basic frr_exporter implementation (#4150) --- .../templates/frr_exporter/frr_exporter.service.j2 | 20 ++++ debian/control | 3 + .../service_monitoring_frr_exporter.xml.in | 25 +++++ .../cli/test_service_monitoring_frr-exporter.py | 64 +++++++++++++ src/conf_mode/service_monitoring_frr-exporter.py | 101 +++++++++++++++++++++ 5 files changed, 213 insertions(+) create mode 100644 data/templates/frr_exporter/frr_exporter.service.j2 create mode 100644 interface-definitions/service_monitoring_frr_exporter.xml.in create mode 100755 smoketest/scripts/cli/test_service_monitoring_frr-exporter.py create mode 100755 src/conf_mode/service_monitoring_frr-exporter.py diff --git a/data/templates/frr_exporter/frr_exporter.service.j2 b/data/templates/frr_exporter/frr_exporter.service.j2 new file mode 100644 index 000000000..c3892e42b --- /dev/null +++ b/data/templates/frr_exporter/frr_exporter.service.j2 @@ -0,0 +1,20 @@ +{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' runuser -u frr -- ' if vrf is vyos_defined else '' %} +[Unit] +Description=FRR Exporter +Documentation=https://github.com/tynany/frr_exporter +After=network.target + +[Service] +{% if vrf is not vyos_defined %} +User=frr +{% endif %} +ExecStart={{ vrf_command }}/usr/sbin/frr_exporter \ +{% if listen_address is vyos_defined %} +{% for address in listen_address %} + --web.listen-address={{ address }}:{{ port }} +{% endfor %} +{% else %} + --web.listen-address=:{{ port }} +{% endif %} +[Install] +WantedBy=multi-user.target diff --git a/debian/control b/debian/control index 20cfcdc43..a19461412 100644 --- a/debian/control +++ b/debian/control @@ -238,6 +238,9 @@ Depends: # For "service monitoring node-exporter" node-exporter, # End "service monitoring node-exporter" +# For "service monitoring frr-exporter" + frr-exporter, +# End "service monitoring frr-exporter" # For "service monitoring telegraf" telegraf (>= 1.20), # End "service monitoring telegraf" diff --git a/interface-definitions/service_monitoring_frr_exporter.xml.in b/interface-definitions/service_monitoring_frr_exporter.xml.in new file mode 100644 index 000000000..96aee3ab4 --- /dev/null +++ b/interface-definitions/service_monitoring_frr_exporter.xml.in @@ -0,0 +1,25 @@ + + + + + + + + + Prometheus exporter for FRR metrics + 1280 + + + #include + #include + + 9342 + + #include + + + + + + + diff --git a/smoketest/scripts/cli/test_service_monitoring_frr-exporter.py b/smoketest/scripts/cli/test_service_monitoring_frr-exporter.py new file mode 100755 index 000000000..230171c11 --- /dev/null +++ b/smoketest/scripts/cli/test_service_monitoring_frr-exporter.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 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 . + +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.utils.process import process_named_running +from vyos.utils.file import read_file + +PROCESS_NAME = 'frr_exporter' +base_path = ['service', 'monitoring', 'frr-exporter'] +service_file = '/etc/systemd/system/frr_exporter.service' +listen_if = 'dum3421' +listen_ip = '192.0.2.1' + + +class TestMonitoringFrrExporter(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + # call base-classes classmethod + super(TestMonitoringFrrExporter, cls).setUpClass() + # create a test interfaces + cls.cli_set( + cls, ['interfaces', 'dummy', listen_if, 'address', listen_ip + '/32'] + ) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['interfaces', 'dummy', listen_if]) + super(TestMonitoringFrrExporter, cls).tearDownClass() + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + self.assertFalse(process_named_running(PROCESS_NAME)) + + def test_01_basic_config(self): + self.cli_set(base_path + ['listen-address', listen_ip]) + + # commit changes + self.cli_commit() + + file_content = read_file(service_file) + self.assertIn(f'{listen_ip}:9342', file_content) + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/src/conf_mode/service_monitoring_frr-exporter.py b/src/conf_mode/service_monitoring_frr-exporter.py new file mode 100755 index 000000000..01527d579 --- /dev/null +++ b/src/conf_mode/service_monitoring_frr-exporter.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 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 . + +import os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import is_node_changed +from vyos.configverify import verify_vrf +from vyos.template import render +from vyos.utils.process import call +from vyos import ConfigError +from vyos import airbag + + +airbag.enable() + +service_file = '/etc/systemd/system/frr_exporter.service' +systemd_service = 'frr_exporter.service' + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['service', 'monitoring', 'frr-exporter'] + if not conf.exists(base): + return None + + config_data = conf.get_config_dict( + base, key_mangling=('-', '_'), get_first_key=True + ) + config_data = conf.merge_defaults(config_data, recursive=True) + + tmp = is_node_changed(conf, base + ['vrf']) + if tmp: + config_data.update({'restart_required': {}}) + + return config_data + + +def verify(config_data): + # bail out early - looks like removal from running config + if not config_data: + return None + + verify_vrf(config_data) + return None + + +def generate(config_data): + if not config_data: + # Delete systemd files + if os.path.isfile(service_file): + os.unlink(service_file) + return None + + # Render frr_exporter service_file + render(service_file, 'frr_exporter/frr_exporter.service.j2', config_data) + return None + + +def apply(config_data): + # Reload systemd manager configuration + call('systemctl daemon-reload') + if not config_data: + call(f'systemctl stop {systemd_service}') + return + + # we need to restart the service if e.g. the VRF name changed + systemd_action = 'reload-or-restart' + if 'restart_required' in config_data: + systemd_action = 'restart' + + call(f'systemctl {systemd_action} {systemd_service}') + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) -- cgit v1.2.3 From 8943446c8c79c6daad2326ddc7a59950123b35d2 Mon Sep 17 00:00:00 2001 From: Nataliia Solomko Date: Fri, 18 Oct 2024 15:42:59 +0300 Subject: pki: T4914: Rewrite the PKI op mode in the new style --- op-mode-definitions/pki.xml.in | 108 +++++++------- python/vyos/configsession.py | 3 +- python/vyos/opmode.py | 5 +- src/op_mode/pki.py | 295 +++++++++++++++++++++------------------ src/services/api/rest/routers.py | 4 +- 5 files changed, 224 insertions(+), 191 deletions(-) diff --git a/op-mode-definitions/pki.xml.in b/op-mode-definitions/pki.xml.in index 254ef08cc..866f482bf 100644 --- a/op-mode-definitions/pki.xml.in +++ b/op-mode-definitions/pki.xml.in @@ -27,7 +27,7 @@ <filename> - sudo -E ${vyos_op_scripts_dir}/pki.py --action generate --ca "$7" --sign "$5" --file + sudo -E ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type ca --name "$7" --sign "$5" --file @@ -36,10 +36,10 @@ <certificate name> - ${vyos_op_scripts_dir}/pki.py --action generate --ca "$7" --sign "$5" --install + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type ca --name "$7" --sign "$5" --install - ${vyos_op_scripts_dir}/pki.py --action generate --ca "noname" --sign "$5" + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type ca --sign "$5" @@ -48,7 +48,7 @@ <filename> - sudo -E ${vyos_op_scripts_dir}/pki.py --action generate --ca "$5" --file + sudo -E ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type ca --name "$5" --file @@ -57,10 +57,10 @@ <CA name> - ${vyos_op_scripts_dir}/pki.py --action generate --ca "$5" --install + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type ca --name "$5" --install - ${vyos_op_scripts_dir}/pki.py --action generate --ca "noname" + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type ca @@ -79,7 +79,7 @@ <filename> - sudo -E ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$6" --self-sign --file + sudo -E ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type certificate --name "$6" --self-sign --file @@ -88,10 +88,10 @@ <certificate name> - ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$6" --self-sign --install + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type certificate --name "$6" --self-sign --install - ${vyos_op_scripts_dir}/pki.py --action generate --certificate "noname" --self-sign + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type certificate --self-sign @@ -108,7 +108,7 @@ <filename> - sudo -E ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$7" --sign "$5" --file + sudo -E ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type certificate --name "$7" --sign "$5" --file @@ -117,10 +117,10 @@ <certificate name> - ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$7" --sign "$5" --install + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type certificate --name "$7" --sign "$5" --install - ${vyos_op_scripts_dir}/pki.py --action generate --certificate "noname" --sign "$5" + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type certificate --sign "$5" @@ -129,7 +129,7 @@ <filename> - sudo -E ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$5" --file + sudo -E ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type certificate --name "$5" --file @@ -138,10 +138,10 @@ <certificate name> - ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$5" --install + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type certificate --name "$5" --install - ${vyos_op_scripts_dir}/pki.py --action generate --certificate "noname" + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type certificate @@ -158,16 +158,16 @@ <filename> - sudo -E ${vyos_op_scripts_dir}/pki.py --action generate --crl "$4" --file + sudo -E ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type crl --name "$4" --file Commands for installing generated CRL into running configuration - ${vyos_op_scripts_dir}/pki.py --action generate --crl "$4" --install + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type crl --name "$4" --install - ${vyos_op_scripts_dir}/pki.py --action generate --crl "$4" + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type crl --name "$4" @@ -181,7 +181,7 @@ <filename> - sudo -E ${vyos_op_scripts_dir}/pki.py --action generate --dh "$5" --file + sudo -E ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type dh --name "$5" --file @@ -190,10 +190,10 @@ <DH name> - ${vyos_op_scripts_dir}/pki.py --action generate --dh "$5" --install + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type dh --name "$5" --install - ${vyos_op_scripts_dir}/pki.py --action generate --dh "noname" + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type dh @@ -207,7 +207,7 @@ <filename> - sudo -E ${vyos_op_scripts_dir}/pki.py --action generate --keypair "$5" --file + sudo -E ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type key-pair --name "$5" --file @@ -216,10 +216,10 @@ <key name> - ${vyos_op_scripts_dir}/pki.py --action generate --keypair "$5" --install + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type key-pair --name "$5" --install - ${vyos_op_scripts_dir}/pki.py --action generate --keypair "noname" + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type key-pair @@ -238,7 +238,7 @@ <filename> - sudo -E ${vyos_op_scripts_dir}/pki.py --action generate --openvpn "$6" --file + sudo -E ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type openvpn --name "$6" --file @@ -247,10 +247,10 @@ <key name> - ${vyos_op_scripts_dir}/pki.py --action generate --openvpn "$6" --install + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type openvpn --name "$6" --install - ${vyos_op_scripts_dir}/pki.py --action generate --openvpn "noname" + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type openvpn @@ -266,7 +266,7 @@ <filename> - sudo -E ${vyos_op_scripts_dir}/pki.py --action generate --ssh "$5" --file + sudo -E ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type ssh --name "$5" --file @@ -275,10 +275,10 @@ <key name> - ${vyos_op_scripts_dir}/pki.py --action generate --ssh "$5" --install + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type ssh --name "$5" --install - ${vyos_op_scripts_dir}/pki.py --action generate --ssh "noname" + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type ssh @@ -302,12 +302,12 @@ interfaces wireguard - ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --key --interface "$7" --install + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type wireguard --key --interface "$7" --install - ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --key + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type wireguard --key @@ -334,14 +334,14 @@ interfaces wireguard ${COMP_WORDS[COMP_CWORD-2]} peer - ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --psk --interface "$7" --peer "$9" --install + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type wireguard --psk --interface "$7" --peer "$9" --install - ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --psk + ${vyos_op_scripts_dir}/pki.py generate_pki --pki-type wireguard --psk @@ -371,13 +371,13 @@ Path to CA certificate file - sudo -E ${vyos_op_scripts_dir}/pki.py --action import --ca "$4" --filename "$6" + sudo -E ${vyos_op_scripts_dir}/pki.py import_pki --pki-type ca --name "$4" --filename "$6" Path to private key file - sudo -E ${vyos_op_scripts_dir}/pki.py --action import --ca "$4" --key-filename "$6" + sudo -E ${vyos_op_scripts_dir}/pki.py import_pki --pki-type ca --name "$4" --key-filename "$6" @@ -393,13 +393,13 @@ Path to certificate file - sudo -E ${vyos_op_scripts_dir}/pki.py --action import --certificate "$4" --filename "$6" + sudo -E ${vyos_op_scripts_dir}/pki.py import_pki --pki-type certificate --name "$4" --filename "$6" Path to private key file - sudo -E ${vyos_op_scripts_dir}/pki.py --action import --certificate "$4" --key-filename "$6" + sudo -E ${vyos_op_scripts_dir}/pki.py import_pki --pki-type certificate --name "$4" --key-filename "$6" @@ -415,7 +415,7 @@ Path to CRL file - sudo -E ${vyos_op_scripts_dir}/pki.py --action import --crl "$4" --filename "$6" + sudo -E ${vyos_op_scripts_dir}/pki.py import_pki --pki-type crl --name "$4" --filename "$6" @@ -431,7 +431,7 @@ Path to DH parameters file - sudo -E ${vyos_op_scripts_dir}/pki.py --action import --dh "$4" --filename "$6" + sudo -E ${vyos_op_scripts_dir}/pki.py import_pki --pki-type dh --name "$4" --filename "$6" @@ -447,13 +447,13 @@ Path to public key file - sudo -E ${vyos_op_scripts_dir}/pki.py --action import --keypair "$4" --filename "$6" + sudo -E ${vyos_op_scripts_dir}/pki.py import_pki --pki-type key-pair --name "$4" --filename "$6" Path to private key file - sudo -E ${vyos_op_scripts_dir}/pki.py --action import --keypair "$4" --key-filename "$6" + sudo -E ${vyos_op_scripts_dir}/pki.py import_pki --pki-type key-pair --name "$4" --key-filename "$6" @@ -474,7 +474,7 @@ Path to shared secret key file - sudo -E ${vyos_op_scripts_dir}/pki.py --action import --openvpn "$5" --filename "$7" + sudo -E ${vyos_op_scripts_dir}/pki.py import_pki --pki-type openvpn --name "$5" --filename "$7" @@ -490,13 +490,13 @@ Show PKI x509 certificates - sudo ${vyos_op_scripts_dir}/pki.py --action show + sudo ${vyos_op_scripts_dir}/pki.py show_all Show x509 CA certificates - sudo ${vyos_op_scripts_dir}/pki.py --action show --ca "all" + sudo ${vyos_op_scripts_dir}/pki.py show_certificate_authority @@ -505,13 +505,13 @@ pki ca - sudo ${vyos_op_scripts_dir}/pki.py --action show --ca "$4" + sudo ${vyos_op_scripts_dir}/pki.py show_certificate_authority --name "$4" Show x509 CA certificate in PEM format - sudo ${vyos_op_scripts_dir}/pki.py --action show --ca "$4" --pem + sudo ${vyos_op_scripts_dir}/pki.py show_certificate_authority --name "$4" --pem @@ -519,7 +519,7 @@ Show x509 certificates - sudo ${vyos_op_scripts_dir}/pki.py --action show --certificate "all" + sudo ${vyos_op_scripts_dir}/pki.py show_certificate @@ -528,13 +528,13 @@ pki certificate - sudo ${vyos_op_scripts_dir}/pki.py --action show --certificate "$4" + sudo ${vyos_op_scripts_dir}/pki.py show_certificate --name "$4" Show x509 certificate in PEM format - sudo ${vyos_op_scripts_dir}/pki.py --action show --certificate "$4" --pem + sudo ${vyos_op_scripts_dir}/pki.py show_certificate --name "$4" --pem @@ -543,7 +543,7 @@ sha256 sha384 sha512 - sudo ${vyos_op_scripts_dir}/pki.py --action show --certificate "$4" --fingerprint "$6" + sudo ${vyos_op_scripts_dir}/pki.py show_certificate --name "$4" --fingerprint "$6" @@ -551,7 +551,7 @@ Show x509 certificate revocation lists - ${vyos_op_scripts_dir}/pki.py --action show --crl "all" + ${vyos_op_scripts_dir}/pki.py show_crl @@ -560,13 +560,13 @@ pki ca - ${vyos_op_scripts_dir}/pki.py --action show --crl "$4" + ${vyos_op_scripts_dir}/pki.py show_crl --name "$4" Show x509 certificate revocation lists by CA name in PEM format - ${vyos_op_scripts_dir}/pki.py --action show --crl "$4" --pem + ${vyos_op_scripts_dir}/pki.py show_crl --name "$4" --pem diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 9c56d246a..5876dc5b0 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -42,8 +42,7 @@ INSTALL_IMAGE = [ IMPORT_PKI = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'import'] IMPORT_PKI_NO_PROMPT = [ '/usr/libexec/vyos/op_mode/pki.py', - '--action', - 'import', + 'import_pki', '--no-prompt', ] REMOVE_IMAGE = [ diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 066c8058f..136ac35e2 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -89,7 +89,10 @@ class InternalError(Error): def _is_op_mode_function_name(name): - if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set|renew|release|execute)", name): + if re.match( + r'^(show|clear|reset|restart|add|update|delete|generate|set|renew|release|execute|import)', + name, + ): return True else: return False diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py index 5652a5d74..56b873bb1 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -14,16 +14,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import argparse import ipaddress import os import re import sys import tabulate +import typing from cryptography import x509 from cryptography.x509.oid import ExtendedKeyUsageOID +import vyos.opmode + from vyos.config import Config from vyos.config import config_dict_mangle_acme from vyos.pki import encode_certificate @@ -51,8 +53,36 @@ from vyos.utils.process import cmd CERT_REQ_END = '-----END CERTIFICATE REQUEST-----' auth_dir = '/config/auth' +ArgsPkiType = typing.Literal['ca', 'certificate', 'dh', 'key-pair', 'openvpn', 'crl'] +ArgsPkiTypeGen = typing.Literal[ArgsPkiType, typing.Literal['ssh', 'wireguard']] +ArgsFingerprint = typing.Literal['sha256', 'sha384', 'sha512'] + # Helper Functions conf = Config() + + +def _verify(target): + """Decorator checks if config for PKI exists""" + from functools import wraps + + if target not in ['ca', 'certificate']: + raise ValueError('Invalid PKI') + + def _verify_target(func): + @wraps(func) + def _wrapper(*args, **kwargs): + name = kwargs.get('name') + unconf_message = f'PKI {target} "{name}" does not exist!' + if name: + if not conf.exists(['pki', target, name]): + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + return func(*args, **kwargs) + + return _wrapper + + return _verify_target + + def get_default_values(): # Fetch default x509 values base = ['pki', 'x509', 'default'] @@ -872,8 +902,111 @@ def import_openvpn_secret(name, path): install_openvpn_key(name, key_data, key_version) -# Show functions -def show_certificate_authority(name=None, pem=False): + +def generate_pki( + raw: bool, + pki_type: ArgsPkiTypeGen, + name: typing.Optional[str], + file: typing.Optional[bool], + install: typing.Optional[bool], + sign: typing.Optional[str], + self_sign: typing.Optional[bool], + key: typing.Optional[bool], + psk: typing.Optional[bool], + interface: typing.Optional[str], + peer: typing.Optional[str], +): + try: + if pki_type == 'ca': + if sign: + generate_ca_certificate_sign(name, sign, install=install, file=file) + else: + generate_ca_certificate(name, install=install, file=file) + elif pki_type == 'certificate': + if sign: + generate_certificate_sign(name, sign, install=install, file=file) + elif self_sign: + generate_certificate_selfsign(name, install=install, file=file) + else: + generate_certificate_request(name=name, install=install, file=file) + + elif pki_type == 'crl': + generate_certificate_revocation_list(name, install=install, file=file) + + elif pki_type == 'ssh': + generate_ssh_keypair(name, install=install, file=file) + + elif pki_type == 'dh': + generate_dh_parameters(name, install=install, file=file) + + elif pki_type == 'key-pair': + generate_keypair(name, install=install, file=file) + + elif pki_type == 'openvpn': + generate_openvpn_key(name, install=install, file=file) + + elif pki_type == 'wireguard': + # WireGuard supports writing key directly into the CLI, but this + # requires the vyos_libexec_dir environment variable to be set + os.environ['vyos_libexec_dir'] = '/usr/libexec/vyos' + + if key: + generate_wireguard_key(interface, install=install) + if psk: + generate_wireguard_psk(interface, peer=peer, install=install) + except KeyboardInterrupt: + print('Aborted') + sys.exit(0) + + +def import_pki( + name: str, + pki_type: ArgsPkiType, + filename: typing.Optional[str], + key_filename: typing.Optional[str], + no_prompt: typing.Optional[bool], + passphrase: typing.Optional[str], +): + try: + if pki_type == 'ca': + import_ca_certificate( + name, + path=filename, + key_path=key_filename, + no_prompt=no_prompt, + passphrase=passphrase, + ) + elif pki_type == 'certificate': + import_certificate( + name, + path=filename, + key_path=key_filename, + no_prompt=no_prompt, + passphrase=passphrase, + ) + elif pki_type == 'crl': + import_crl(name, filename) + elif pki_type == 'dh': + import_dh_parameters(name, filename) + elif pki_type == 'key-pair': + import_keypair( + name, + path=filename, + key_path=key_filename, + no_prompt=no_prompt, + passphrase=passphrase, + ) + elif pki_type == 'openvpn': + import_openvpn_secret(name, filename) + except KeyboardInterrupt: + print('Aborted') + sys.exit(0) + + +@_verify('ca') +def show_certificate_authority( + raw: bool, name: typing.Optional[str] = None, pem: typing.Optional[bool] = False +): headers = ['Name', 'Subject', 'Issuer CN', 'Issued', 'Expiry', 'Private Key', 'Parent'] data = [] certs = get_config_ca_certificate() @@ -905,7 +1038,14 @@ def show_certificate_authority(name=None, pem=False): print("Certificate Authorities:") print(tabulate.tabulate(data, headers)) -def show_certificate(name=None, pem=False, fingerprint_hash=None): + +@_verify('certificate') +def show_certificate( + raw: bool, + name: typing.Optional[str] = None, + pem: typing.Optional[bool] = False, + fingerprint: typing.Optional[ArgsFingerprint] = None, +): headers = ['Name', 'Type', 'Subject CN', 'Issuer CN', 'Issued', 'Expiry', 'Revoked', 'Private Key', 'CA Present'] data = [] certs = get_config_certificate() @@ -926,8 +1066,8 @@ def show_certificate(name=None, pem=False, fingerprint_hash=None): if name and pem: print(encode_certificate(cert)) return - elif name and fingerprint_hash: - print(get_certificate_fingerprint(cert, fingerprint_hash)) + elif name and fingerprint: + print(get_certificate_fingerprint(cert, fingerprint)) return ca_name = get_certificate_ca(cert, ca_certs) @@ -955,7 +1095,10 @@ def show_certificate(name=None, pem=False, fingerprint_hash=None): print("Certificates:") print(tabulate.tabulate(data, headers)) -def show_crl(name=None, pem=False): + +def show_crl( + raw: bool, name: typing.Optional[str] = None, pem: typing.Optional[bool] = False +): headers = ['CA Name', 'Updated', 'Revokes'] data = [] certs = get_config_ca_certificate() @@ -989,132 +1132,20 @@ def show_crl(name=None, pem=False): print("Certificate Revocation Lists:") print(tabulate.tabulate(data, headers)) -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('--action', help='PKI action', required=True) - - # X509 - parser.add_argument('--ca', help='Certificate Authority', required=False) - parser.add_argument('--certificate', help='Certificate', required=False) - parser.add_argument('--crl', help='Certificate Revocation List', required=False) - parser.add_argument('--sign', help='Sign certificate with specified CA', required=False) - parser.add_argument('--self-sign', help='Self-sign the certificate', action='store_true') - parser.add_argument('--pem', help='Output using PEM encoding', action='store_true') - parser.add_argument('--fingerprint', help='Show fingerprint and exit', action='store') - # SSH - parser.add_argument('--ssh', help='SSH Key', required=False) +def show_all(raw: bool): + show_certificate_authority(raw) + print('\n') + show_certificate(raw) + print('\n') + show_crl(raw) - # DH - parser.add_argument('--dh', help='DH Parameters', required=False) - - # Key pair - parser.add_argument('--keypair', help='Key pair', required=False) - - # OpenVPN - parser.add_argument('--openvpn', help='OpenVPN TLS key', required=False) - - # WireGuard - parser.add_argument('--wireguard', help='Wireguard', action='store_true') - group = parser.add_mutually_exclusive_group() - group.add_argument('--key', help='Wireguard key pair', action='store_true', required=False) - group.add_argument('--psk', help='Wireguard pre shared key', action='store_true', required=False) - parser.add_argument('--interface', help='Install generated keys into running-config for named interface', action='store') - parser.add_argument('--peer', help='Install generated keys into running-config for peer', action='store') - - # Global - parser.add_argument('--file', help='Write generated keys into specified filename', action='store_true') - parser.add_argument('--install', help='Install generated keys into running-config', action='store_true') - - parser.add_argument('--filename', help='Write certificate into specified filename', action='store') - parser.add_argument('--key-filename', help='Write key into specified filename', action='store') - - parser.add_argument('--no-prompt', action='store_true', help='Perform action non-interactively') - parser.add_argument('--passphrase', help='A passphrase to decrypt the private key') - - args = parser.parse_args() +if __name__ == '__main__': try: - if args.action == 'generate': - if args.ca: - if args.sign: - generate_ca_certificate_sign(args.ca, args.sign, install=args.install, file=args.file) - else: - generate_ca_certificate(args.ca, install=args.install, file=args.file) - elif args.certificate: - if args.sign: - generate_certificate_sign(args.certificate, args.sign, install=args.install, file=args.file) - elif args.self_sign: - generate_certificate_selfsign(args.certificate, install=args.install, file=args.file) - else: - generate_certificate_request(name=args.certificate, install=args.install, file=args.file) - - elif args.crl: - generate_certificate_revocation_list(args.crl, install=args.install, file=args.file) - - elif args.ssh: - generate_ssh_keypair(args.ssh, install=args.install, file=args.file) - - elif args.dh: - generate_dh_parameters(args.dh, install=args.install, file=args.file) - - elif args.keypair: - generate_keypair(args.keypair, install=args.install, file=args.file) - - elif args.openvpn: - generate_openvpn_key(args.openvpn, install=args.install, file=args.file) - - elif args.wireguard: - # WireGuard supports writing key directly into the CLI, but this - # requires the vyos_libexec_dir environment variable to be set - os.environ["vyos_libexec_dir"] = "/usr/libexec/vyos" - - if args.key: - generate_wireguard_key(args.interface, install=args.install) - if args.psk: - generate_wireguard_psk(args.interface, peer=args.peer, install=args.install) - elif args.action == 'import': - if args.ca: - import_ca_certificate(args.ca, path=args.filename, key_path=args.key_filename, - no_prompt=args.no_prompt, passphrase=args.passphrase) - elif args.certificate: - import_certificate(args.certificate, path=args.filename, key_path=args.key_filename, - no_prompt=args.no_prompt, passphrase=args.passphrase) - elif args.crl: - import_crl(args.crl, args.filename) - elif args.dh: - import_dh_parameters(args.dh, args.filename) - elif args.keypair: - import_keypair(args.keypair, path=args.filename, key_path=args.key_filename, - no_prompt=args.no_prompt, passphrase=args.passphrase) - elif args.openvpn: - import_openvpn_secret(args.openvpn, args.filename) - elif args.action == 'show': - if args.ca: - ca_name = None if args.ca == 'all' else args.ca - if ca_name: - if not conf.exists(['pki', 'ca', ca_name]): - print(f'CA "{ca_name}" does not exist!') - exit(1) - show_certificate_authority(ca_name, args.pem) - elif args.certificate: - cert_name = None if args.certificate == 'all' else args.certificate - if cert_name: - if not conf.exists(['pki', 'certificate', cert_name]): - print(f'Certificate "{cert_name}" does not exist!') - exit(1) - if args.fingerprint is None: - show_certificate(None if args.certificate == 'all' else args.certificate, args.pem) - else: - show_certificate(args.certificate, fingerprint_hash=args.fingerprint) - elif args.crl: - show_crl(None if args.crl == 'all' else args.crl, args.pem) - else: - show_certificate_authority() - print('\n') - show_certificate() - print('\n') - show_crl() - except KeyboardInterrupt: - print("Aborted") - sys.exit(0) + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/services/api/rest/routers.py b/src/services/api/rest/routers.py index da981d5bf..5612e947c 100644 --- a/src/services/api/rest/routers.py +++ b/src/services/api/rest/routers.py @@ -423,9 +423,9 @@ def create_path_import_pki_no_prompt(path): correct_paths = ['ca', 'certificate', 'key-pair'] if path[1] not in correct_paths: return False - path[1] = '--' + path[1].replace('-', '') path[3] = '--key-filename' - return path[1:] + path.insert(2, '--name') + return ['--pki-type'] + path[1:] @router.post('/configure') -- cgit v1.2.3 From 52e3ba35c09237744340c2cce4cba1783ec3f2be Mon Sep 17 00:00:00 2001 From: Nataliia Solomko Date: Fri, 18 Oct 2024 16:46:39 +0300 Subject: pki: T4914: reformat file by linter rules --- src/op_mode/pki.py | 517 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 375 insertions(+), 142 deletions(-) diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py index 56b873bb1..49a461e9e 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -86,13 +86,17 @@ def _verify(target): def get_default_values(): # Fetch default x509 values base = ['pki', 'x509', 'default'] - x509_defaults = conf.get_config_dict(base, key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - get_first_key=True, - with_recursive_defaults=True) + x509_defaults = conf.get_config_dict( + base, + key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + get_first_key=True, + with_recursive_defaults=True, + ) return x509_defaults + def get_config_ca_certificate(name=None): # Fetch ca certificates from config base = ['pki', 'ca'] @@ -101,12 +105,15 @@ def get_config_ca_certificate(name=None): if name: base = base + [name] - if not conf.exists(base + ['private', 'key']) or not conf.exists(base + ['certificate']): + if not conf.exists(base + ['private', 'key']) or not conf.exists( + base + ['certificate'] + ): return False - return conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) + return conf.get_config_dict( + base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True + ) + def get_config_certificate(name=None): # Get certificates from config @@ -116,18 +123,21 @@ def get_config_certificate(name=None): if name: base = base + [name] - if not conf.exists(base + ['private', 'key']) or not conf.exists(base + ['certificate']): + if not conf.exists(base + ['private', 'key']) or not conf.exists( + base + ['certificate'] + ): return False - pki = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) + pki = conf.get_config_dict( + base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True + ) if pki: for certificate in pki: pki[certificate] = config_dict_mangle_acme(certificate, pki[certificate]) return pki + def get_certificate_ca(cert, ca_certs): # Find CA certificate for given certificate if not ca_certs: @@ -146,6 +156,7 @@ def get_certificate_ca(cert, ca_certs): return ca_name return None + def get_config_revoked_certificates(): # Fetch revoked certificates from config ca_base = ['pki', 'ca'] @@ -154,19 +165,26 @@ def get_config_revoked_certificates(): certs = [] if conf.exists(ca_base): - ca_certificates = conf.get_config_dict(ca_base, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) + ca_certificates = conf.get_config_dict( + ca_base, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) certs.extend(ca_certificates.values()) if conf.exists(cert_base): - certificates = conf.get_config_dict(cert_base, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) + certificates = conf.get_config_dict( + cert_base, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) certs.extend(certificates.values()) return [cert_dict for cert_dict in certs if 'revoke' in cert_dict] + def get_revoked_by_serial_numbers(serial_numbers=[]): # Return serial numbers of revoked certificates certs_out = [] @@ -190,113 +208,153 @@ def get_revoked_by_serial_numbers(serial_numbers=[]): certs_out.append(cert_name) return certs_out -def install_certificate(name, cert='', private_key=None, key_type=None, key_passphrase=None, is_ca=False): + +def install_certificate( + name, cert='', private_key=None, key_type=None, key_passphrase=None, is_ca=False +): # Show/install conf commands for certificate prefix = 'ca' if is_ca else 'certificate' - base = f"pki {prefix} {name}" + base = f'pki {prefix} {name}' config_paths = [] if cert: - cert_pem = "".join(encode_certificate(cert).strip().split("\n")[1:-1]) + cert_pem = ''.join(encode_certificate(cert).strip().split('\n')[1:-1]) config_paths.append(f"{base} certificate '{cert_pem}'") if private_key: - key_pem = "".join(encode_private_key(private_key, passphrase=key_passphrase).strip().split("\n")[1:-1]) + key_pem = ''.join( + encode_private_key(private_key, passphrase=key_passphrase) + .strip() + .split('\n')[1:-1] + ) config_paths.append(f"{base} private key '{key_pem}'") if key_passphrase: - config_paths.append(f"{base} private password-protected") + config_paths.append(f'{base} private password-protected') install_into_config(conf, config_paths) + def install_crl(ca_name, crl): # Show/install conf commands for crl - crl_pem = "".join(encode_certificate(crl).strip().split("\n")[1:-1]) + crl_pem = ''.join(encode_certificate(crl).strip().split('\n')[1:-1]) install_into_config(conf, [f"pki ca {ca_name} crl '{crl_pem}'"]) + def install_dh_parameters(name, params): # Show/install conf commands for dh params - dh_pem = "".join(encode_dh_parameters(params).strip().split("\n")[1:-1]) + dh_pem = ''.join(encode_dh_parameters(params).strip().split('\n')[1:-1]) install_into_config(conf, [f"pki dh {name} parameters '{dh_pem}'"]) + def install_ssh_key(name, public_key, private_key, passphrase=None): # Show/install conf commands for ssh key - key_openssh = encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH') + key_openssh = encode_public_key( + public_key, encoding='OpenSSH', key_format='OpenSSH' + ) username = os.getlogin() - type_key_split = key_openssh.split(" ") - - base = f"system login user {username} authentication public-keys {name}" - install_into_config(conf, [ - f"{base} key '{type_key_split[1]}'", - f"{base} type '{type_key_split[0]}'" - ]) - print(encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase)) - -def install_keypair(name, key_type, private_key=None, public_key=None, passphrase=None, prompt=True): + type_key_split = key_openssh.split(' ') + + base = f'system login user {username} authentication public-keys {name}' + install_into_config( + conf, + [f"{base} key '{type_key_split[1]}'", f"{base} type '{type_key_split[0]}'"], + ) + print( + encode_private_key( + private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase + ) + ) + + +def install_keypair( + name, key_type, private_key=None, public_key=None, passphrase=None, prompt=True +): # Show/install conf commands for key-pair config_paths = [] if public_key: - install_public_key = not prompt or ask_yes_no('Do you want to install the public key?', default=True) + install_public_key = not prompt or ask_yes_no( + 'Do you want to install the public key?', default=True + ) public_key_pem = encode_public_key(public_key) if install_public_key: - install_public_pem = "".join(public_key_pem.strip().split("\n")[1:-1]) - config_paths.append(f"pki key-pair {name} public key '{install_public_pem}'") + install_public_pem = ''.join(public_key_pem.strip().split('\n')[1:-1]) + config_paths.append( + f"pki key-pair {name} public key '{install_public_pem}'" + ) else: - print("Public key:") + print('Public key:') print(public_key_pem) if private_key: - install_private_key = not prompt or ask_yes_no('Do you want to install the private key?', default=True) + install_private_key = not prompt or ask_yes_no( + 'Do you want to install the private key?', default=True + ) private_key_pem = encode_private_key(private_key, passphrase=passphrase) if install_private_key: - install_private_pem = "".join(private_key_pem.strip().split("\n")[1:-1]) - config_paths.append(f"pki key-pair {name} private key '{install_private_pem}'") + install_private_pem = ''.join(private_key_pem.strip().split('\n')[1:-1]) + config_paths.append( + f"pki key-pair {name} private key '{install_private_pem}'" + ) if passphrase: - config_paths.append(f"pki key-pair {name} private password-protected") + config_paths.append(f'pki key-pair {name} private password-protected') else: - print("Private key:") + print('Private key:') print(private_key_pem) install_into_config(conf, config_paths) + def install_openvpn_key(name, key_data, key_version='1'): config_paths = [ f"pki openvpn shared-secret {name} key '{key_data}'", - f"pki openvpn shared-secret {name} version '{key_version}'" + f"pki openvpn shared-secret {name} version '{key_version}'", ] install_into_config(conf, config_paths) + def install_wireguard_key(interface, private_key, public_key): # Show conf commands for installing wireguard key pairs from vyos.ifconfig import Section + if Section.section(interface) != 'wireguard': print(f'"{interface}" is not a WireGuard interface name!') exit(1) # Check if we are running in a config session - if yes, we can directly write to the CLI - install_into_config(conf, [f"interfaces wireguard {interface} private-key '{private_key}'"]) + install_into_config( + conf, [f"interfaces wireguard {interface} private-key '{private_key}'"] + ) print(f"Corresponding public-key to use on peer system is: '{public_key}'") + def install_wireguard_psk(interface, peer, psk): from vyos.ifconfig import Section + if Section.section(interface) != 'wireguard': print(f'"{interface}" is not a WireGuard interface name!') exit(1) # Check if we are running in a config session - if yes, we can directly write to the CLI - install_into_config(conf, [f"interfaces wireguard {interface} peer {peer} preshared-key '{psk}'"]) + install_into_config( + conf, [f"interfaces wireguard {interface} peer {peer} preshared-key '{psk}'"] + ) + def ask_passphrase(): passphrase = None - print("Note: If you plan to use the generated key on this router, do not encrypt the private key.") + print( + 'Note: If you plan to use the generated key on this router, do not encrypt the private key.' + ) if ask_yes_no('Do you want to encrypt the private key with a passphrase?'): passphrase = ask_input('Enter passphrase:') return passphrase + def write_file(filename, contents): full_path = os.path.join(auth_dir, filename) directory = os.path.dirname(full_path) @@ -305,7 +363,9 @@ def write_file(filename, contents): print('Failed to write file: directory does not exist') return False - if os.path.exists(full_path) and not ask_yes_no('Do you want to overwrite the existing file?'): + if os.path.exists(full_path) and not ask_yes_no( + 'Do you want to overwrite the existing file?' + ): return False with open(full_path, 'w') as f: @@ -313,10 +373,14 @@ def write_file(filename, contents): print(f'File written to {full_path}') -# Generation functions +# Generation functions def generate_private_key(): - key_type = ask_input('Enter private key type: [rsa, dsa, ec]', default='rsa', valid_responses=['rsa', 'dsa', 'ec']) + key_type = ask_input( + 'Enter private key type: [rsa, dsa, ec]', + default='rsa', + valid_responses=['rsa', 'dsa', 'ec'], + ) size_valid = [] size_default = 0 @@ -328,28 +392,43 @@ def generate_private_key(): size_default = 256 size_valid = [224, 256, 384, 521] - size = ask_input('Enter private key bits:', default=size_default, numeric_only=True, valid_responses=size_valid) + size = ask_input( + 'Enter private key bits:', + default=size_default, + numeric_only=True, + valid_responses=size_valid, + ) return create_private_key(key_type, size), key_type + def parse_san_string(san_string): if not san_string: return None output = [] - san_split = san_string.strip().split(",") + san_split = san_string.strip().split(',') for pair_str in san_split: - tag, value = pair_str.strip().split(":", 1) + tag, value = pair_str.strip().split(':', 1) if tag == 'ipv4': output.append(ipaddress.IPv4Address(value)) elif tag == 'ipv6': output.append(ipaddress.IPv6Address(value)) elif tag == 'dns' or tag == 'rfc822': output.append(value) - return output + return -def generate_certificate_request(private_key=None, key_type=None, return_request=False, name=None, install=False, file=False, ask_san=True): + +def generate_certificate_request( + private_key=None, + key_type=None, + return_request=False, + name=None, + install=False, + file=False, + ask_san=True, +): if not private_key: private_key, key_type = generate_private_key() @@ -358,18 +437,24 @@ def generate_certificate_request(private_key=None, key_type=None, return_request while True: country = ask_input('Enter country code:', default=default_values['country']) if len(country) != 2: - print("Country name must be a 2 character country code") + print('Country name must be a 2 character country code') continue subject['country'] = country break subject['state'] = ask_input('Enter state:', default=default_values['state']) - subject['locality'] = ask_input('Enter locality:', default=default_values['locality']) - subject['organization'] = ask_input('Enter organization name:', default=default_values['organization']) + subject['locality'] = ask_input( + 'Enter locality:', default=default_values['locality'] + ) + subject['organization'] = ask_input( + 'Enter organization name:', default=default_values['organization'] + ) subject['common_name'] = ask_input('Enter common name:', default='vyos.io') subject_alt_names = None if ask_san and ask_yes_no('Do you want to configure Subject Alternative Names?'): - print("Enter alternative names in a comma separate list, example: ipv4:1.1.1.1,ipv6:fe80::1,dns:vyos.net,rfc822:user@vyos.net") + print( + 'Enter alternative names in a comma separate list, example: ipv4:1.1.1.1,ipv6:fe80::1,dns:vyos.net,rfc822:user@vyos.net' + ) san_string = ask_input('Enter Subject Alternative Names:') subject_alt_names = parse_san_string(san_string) @@ -386,24 +471,48 @@ def generate_certificate_request(private_key=None, key_type=None, return_request return None if install: - print("Certificate request:") - print(encode_certificate(cert_req) + "\n") - install_certificate(name, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False) + print('Certificate request:') + print(encode_certificate(cert_req) + '\n') + install_certificate( + name, + private_key=private_key, + key_type=key_type, + key_passphrase=passphrase, + is_ca=False, + ) if file: write_file(f'{name}.csr', encode_certificate(cert_req)) - write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) + write_file( + f'{name}.key', encode_private_key(private_key, passphrase=passphrase) + ) -def generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False, is_sub_ca=False): - valid_days = ask_input('Enter how many days certificate will be valid:', default='365' if not is_ca else '1825', numeric_only=True) + +def generate_certificate( + cert_req, ca_cert, ca_private_key, is_ca=False, is_sub_ca=False +): + valid_days = ask_input( + 'Enter how many days certificate will be valid:', + default='365' if not is_ca else '1825', + numeric_only=True, + ) cert_type = None if not is_ca: - cert_type = ask_input('Enter certificate type: (client, server)', default='server', valid_responses=['client', 'server']) - return create_certificate(cert_req, ca_cert, ca_private_key, valid_days, cert_type, is_ca, is_sub_ca) + cert_type = ask_input( + 'Enter certificate type: (client, server)', + default='server', + valid_responses=['client', 'server'], + ) + return create_certificate( + cert_req, ca_cert, ca_private_key, valid_days, cert_type, is_ca, is_sub_ca + ) + def generate_ca_certificate(name, install=False, file=False): private_key, key_type = generate_private_key() - cert_req = generate_certificate_request(private_key, key_type, return_request=True, ask_san=False) + cert_req = generate_certificate_request( + private_key, key_type, return_request=True, ask_san=False + ) cert = generate_certificate(cert_req, cert_req, private_key, is_ca=True) passphrase = ask_passphrase() @@ -413,11 +522,16 @@ def generate_ca_certificate(name, install=False, file=False): return None if install: - install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True) + install_certificate( + name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True + ) if file: write_file(f'{name}.pem', encode_certificate(cert)) - write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) + write_file( + f'{name}.key', encode_private_key(private_key, passphrase=passphrase) + ) + def generate_ca_certificate_sign(name, ca_name, install=False, file=False): ca_dict = get_config_ca_certificate(ca_name) @@ -429,17 +543,19 @@ def generate_ca_certificate_sign(name, ca_name, install=False, file=False): ca_cert = load_certificate(ca_dict['certificate']) if not ca_cert: - print("Failed to load signing CA certificate, aborting") + print('Failed to load signing CA certificate, aborting') return None ca_private = ca_dict['private'] ca_private_passphrase = None if 'password_protected' in ca_private: ca_private_passphrase = ask_input('Enter signing CA private key passphrase:') - ca_private_key = load_private_key(ca_private['key'], passphrase=ca_private_passphrase) + ca_private_key = load_private_key( + ca_private['key'], passphrase=ca_private_passphrase + ) if not ca_private_key: - print("Failed to load signing CA private key, aborting") + print('Failed to load signing CA private key, aborting') return None private_key = None @@ -448,9 +564,11 @@ def generate_ca_certificate_sign(name, ca_name, install=False, file=False): cert_req = None if not ask_yes_no('Do you already have a certificate request?'): private_key, key_type = generate_private_key() - cert_req = generate_certificate_request(private_key, key_type, return_request=True, ask_san=False) + cert_req = generate_certificate_request( + private_key, key_type, return_request=True, ask_san=False + ) else: - print("Paste certificate request and press enter:") + print('Paste certificate request and press enter:') lines = [] curr_line = '' while True: @@ -460,17 +578,21 @@ def generate_ca_certificate_sign(name, ca_name, install=False, file=False): lines.append(curr_line) if not lines: - print("Aborted") + print('Aborted') return None - wrap = lines[0].find('-----') < 0 # Only base64 pasted, add the CSR tags for parsing - cert_req = load_certificate_request("\n".join(lines), wrap) + wrap = ( + lines[0].find('-----') < 0 + ) # Only base64 pasted, add the CSR tags for parsing + cert_req = load_certificate_request('\n'.join(lines), wrap) if not cert_req: - print("Invalid certificate request") + print('Invalid certificate request') return None - cert = generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=True, is_sub_ca=True) + cert = generate_certificate( + cert_req, ca_cert, ca_private_key, is_ca=True, is_sub_ca=True + ) passphrase = None if private_key is not None: @@ -483,12 +605,17 @@ def generate_ca_certificate_sign(name, ca_name, install=False, file=False): return None if install: - install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True) + install_certificate( + name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True + ) if file: write_file(f'{name}.pem', encode_certificate(cert)) if private_key is not None: - write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) + write_file( + f'{name}.key', encode_private_key(private_key, passphrase=passphrase) + ) + def generate_certificate_sign(name, ca_name, install=False, file=False): ca_dict = get_config_ca_certificate(ca_name) @@ -500,17 +627,19 @@ def generate_certificate_sign(name, ca_name, install=False, file=False): ca_cert = load_certificate(ca_dict['certificate']) if not ca_cert: - print("Failed to load CA certificate, aborting") + print('Failed to load CA certificate, aborting') return None ca_private = ca_dict['private'] ca_private_passphrase = None if 'password_protected' in ca_private: ca_private_passphrase = ask_input('Enter CA private key passphrase:') - ca_private_key = load_private_key(ca_private['key'], passphrase=ca_private_passphrase) + ca_private_key = load_private_key( + ca_private['key'], passphrase=ca_private_passphrase + ) if not ca_private_key: - print("Failed to load CA private key, aborting") + print('Failed to load CA private key, aborting') return None private_key = None @@ -519,9 +648,11 @@ def generate_certificate_sign(name, ca_name, install=False, file=False): cert_req = None if not ask_yes_no('Do you already have a certificate request?'): private_key, key_type = generate_private_key() - cert_req = generate_certificate_request(private_key, key_type, return_request=True) + cert_req = generate_certificate_request( + private_key, key_type, return_request=True + ) else: - print("Paste certificate request and press enter:") + print('Paste certificate request and press enter:') lines = [] curr_line = '' while True: @@ -531,18 +662,20 @@ def generate_certificate_sign(name, ca_name, install=False, file=False): lines.append(curr_line) if not lines: - print("Aborted") + print('Aborted') return None - wrap = lines[0].find('-----') < 0 # Only base64 pasted, add the CSR tags for parsing - cert_req = load_certificate_request("\n".join(lines), wrap) + wrap = ( + lines[0].find('-----') < 0 + ) # Only base64 pasted, add the CSR tags for parsing + cert_req = load_certificate_request('\n'.join(lines), wrap) if not cert_req: - print("Invalid certificate request") + print('Invalid certificate request') return None cert = generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False) - + passphrase = None if private_key is not None: passphrase = ask_passphrase() @@ -554,12 +687,17 @@ def generate_certificate_sign(name, ca_name, install=False, file=False): return None if install: - install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=False) + install_certificate( + name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=False + ) if file: write_file(f'{name}.pem', encode_certificate(cert)) if private_key is not None: - write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) + write_file( + f'{name}.key', encode_private_key(private_key, passphrase=passphrase) + ) + def generate_certificate_selfsign(name, install=False, file=False): private_key, key_type = generate_private_key() @@ -573,11 +711,21 @@ def generate_certificate_selfsign(name, install=False, file=False): return None if install: - install_certificate(name, cert, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False) + install_certificate( + name, + cert, + private_key=private_key, + key_type=key_type, + key_passphrase=passphrase, + is_ca=False, + ) if file: write_file(f'{name}.pem', encode_certificate(cert)) - write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) + write_file( + f'{name}.key', encode_private_key(private_key, passphrase=passphrase) + ) + def generate_certificate_revocation_list(ca_name, install=False, file=False): ca_dict = get_config_ca_certificate(ca_name) @@ -589,17 +737,19 @@ def generate_certificate_revocation_list(ca_name, install=False, file=False): ca_cert = load_certificate(ca_dict['certificate']) if not ca_cert: - print("Failed to load CA certificate, aborting") + print('Failed to load CA certificate, aborting') return None ca_private = ca_dict['private'] ca_private_passphrase = None if 'password_protected' in ca_private: ca_private_passphrase = ask_input('Enter CA private key passphrase:') - ca_private_key = load_private_key(ca_private['key'], passphrase=ca_private_passphrase) + ca_private_key = load_private_key( + ca_private['key'], passphrase=ca_private_passphrase + ) if not ca_private_key: - print("Failed to load CA private key, aborting") + print('Failed to load CA private key, aborting') return None revoked_certs = get_config_revoked_certificates() @@ -620,13 +770,13 @@ def generate_certificate_revocation_list(ca_name, install=False, file=False): continue if not to_revoke: - print("No revoked certificates to add to the CRL") + print('No revoked certificates to add to the CRL') return None crl = create_certificate_revocation_list(ca_cert, ca_private_key, to_revoke) if not crl: - print("Failed to create CRL") + print('Failed to create CRL') return None if not install and not file: @@ -637,7 +787,8 @@ def generate_certificate_revocation_list(ca_name, install=False, file=False): install_crl(ca_name, crl) if file: - write_file(f'{name}.crl', encode_certificate(crl)) + write_file(f'{ca_name}.crl', encode_certificate(crl)) + def generate_ssh_keypair(name, install=False, file=False): private_key, key_type = generate_private_key() @@ -646,29 +797,42 @@ def generate_ssh_keypair(name, install=False, file=False): if not install and not file: print(encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH')) - print("") - print(encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase)) + print('') + print( + encode_private_key( + private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase + ) + ) return None if install: install_ssh_key(name, public_key, private_key, passphrase) if file: - write_file(f'{name}.pem', encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH')) - write_file(f'{name}.key', encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase)) + write_file( + f'{name}.pem', + encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH'), + ) + write_file( + f'{name}.key', + encode_private_key( + private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase + ), + ) + def generate_dh_parameters(name, install=False, file=False): bits = ask_input('Enter DH parameters key size:', default=2048, numeric_only=True) - print("Generating parameters...") + print('Generating parameters...') dh_params = create_dh_parameters(bits) if not dh_params: - print("Failed to create DH parameters") + print('Failed to create DH parameters') return None if not install and not file: - print("DH Parameters:") + print('DH Parameters:') print(encode_dh_parameters(dh_params)) if install: @@ -677,6 +841,7 @@ def generate_dh_parameters(name, install=False, file=False): if file: write_file(f'{name}.pem', encode_dh_parameters(dh_params)) + def generate_keypair(name, install=False, file=False): private_key, key_type = generate_private_key() public_key = private_key.public_key() @@ -684,7 +849,7 @@ def generate_keypair(name, install=False, file=False): if not install and not file: print(encode_public_key(public_key)) - print("") + print('') print(encode_private_key(private_key, passphrase=passphrase)) return None @@ -693,13 +858,16 @@ def generate_keypair(name, install=False, file=False): if file: write_file(f'{name}.pem', encode_public_key(public_key)) - write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) + write_file( + f'{name}.key', encode_private_key(private_key, passphrase=passphrase) + ) + def generate_openvpn_key(name, install=False, file=False): result = cmd('openvpn --genkey secret /dev/stdout | grep -o "^[^#]*"') if not result: - print("Failed to generate OpenVPN key") + print('Failed to generate OpenVPN key') return None if not install and not file: @@ -707,11 +875,13 @@ def generate_openvpn_key(name, install=False, file=False): return None if install: - key_lines = result.split("\n") - key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings + key_lines = result.split('\n') + key_data = ''.join(key_lines[1:-1]) # Remove wrapper tags and line endings key_version = '1' - version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', result) # Future-proofing (hopefully) + version_search = re.search( + r'BEGIN OpenVPN Static key V(\d+)', result + ) # Future-proofing (hopefully) if version_search: key_version = version_search[1] @@ -720,6 +890,7 @@ def generate_openvpn_key(name, install=False, file=False): if file: write_file(f'{name}.key', result) + def generate_wireguard_key(interface=None, install=False): private_key = cmd('wg genkey') public_key = cmd('wg pubkey', input=private_key) @@ -730,6 +901,7 @@ def generate_wireguard_key(interface=None, install=False): print(f'Private key: {private_key}') print(f'Public key: {public_key}', end='\n\n') + def generate_wireguard_psk(interface=None, peer=None, install=False): psk = cmd('wg genpsk') if interface and peer and install: @@ -737,8 +909,11 @@ def generate_wireguard_psk(interface=None, peer=None, install=False): else: print(f'Pre-shared key: {psk}') + # Import functions -def import_ca_certificate(name, path=None, key_path=None, no_prompt=False, passphrase=None): +def import_ca_certificate( + name, path=None, key_path=None, no_prompt=False, passphrase=None +): if path: if not os.path.exists(path): print(f'File not found: {path}') @@ -775,7 +950,10 @@ def import_ca_certificate(name, path=None, key_path=None, no_prompt=False, passp install_certificate(name, private_key=key, is_ca=True) -def import_certificate(name, path=None, key_path=None, no_prompt=False, passphrase=None): + +def import_certificate( + name, path=None, key_path=None, no_prompt=False, passphrase=None +): if path: if not os.path.exists(path): print(f'File not found: {path}') @@ -812,6 +990,7 @@ def import_certificate(name, path=None, key_path=None, no_prompt=False, passphra install_certificate(name, private_key=key, is_ca=False) + def import_crl(name, path): if not os.path.exists(path): print(f'File not found: {path}') @@ -829,6 +1008,7 @@ def import_crl(name, path): install_crl(name, crl) + def import_dh_parameters(name, path): if not os.path.exists(path): print(f'File not found: {path}') @@ -846,6 +1026,7 @@ def import_dh_parameters(name, path): install_dh_parameters(name, dh) + def import_keypair(name, path=None, key_path=None, no_prompt=False, passphrase=None): if path: if not os.path.exists(path): @@ -883,6 +1064,7 @@ def import_keypair(name, path=None, key_path=None, no_prompt=False, passphrase=N install_keypair(name, None, private_key=key, prompt=False) + def import_openvpn_secret(name, path): if not os.path.exists(path): print(f'File not found: {path}') @@ -892,11 +1074,15 @@ def import_openvpn_secret(name, path): key_version = '1' with open(path) as f: - key_lines = f.read().strip().split("\n") - key_lines = list(filter(lambda line: not line.strip().startswith('#'), key_lines)) # Remove commented lines - key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings - - version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', key_lines[0]) # Future-proofing (hopefully) + key_lines = f.read().strip().split('\n') + key_lines = list( + filter(lambda line: not line.strip().startswith('#'), key_lines) + ) # Remove commented lines + key_data = ''.join(key_lines[1:-1]) # Remove wrapper tags and line endings + + version_search = re.search( + r'BEGIN OpenVPN Static key V(\d+)', key_lines[0] + ) # Future-proofing (hopefully) if version_search: key_version = version_search[1] @@ -1007,7 +1193,15 @@ def import_pki( def show_certificate_authority( raw: bool, name: typing.Optional[str] = None, pem: typing.Optional[bool] = False ): - headers = ['Name', 'Subject', 'Issuer CN', 'Issued', 'Expiry', 'Private Key', 'Parent'] + headers = [ + 'Name', + 'Subject', + 'Issuer CN', + 'Issued', + 'Expiry', + 'Private Key', + 'Parent', + ] data = [] certs = get_config_ca_certificate() if certs: @@ -1024,7 +1218,7 @@ def show_certificate_authority( return parent_ca_name = get_certificate_ca(cert, certs) - cert_issuer_cn = cert.issuer.rfc4514_string().split(",")[0] + cert_issuer_cn = cert.issuer.rfc4514_string().split(',')[0] if not parent_ca_name or parent_ca_name == cert_name: parent_ca_name = 'N/A' @@ -1032,10 +1226,24 @@ def show_certificate_authority( if not cert: continue - have_private = 'Yes' if 'private' in cert_dict and 'key' in cert_dict['private'] else 'No' - data.append([cert_name, cert.subject.rfc4514_string(), cert_issuer_cn, cert.not_valid_before, cert.not_valid_after, have_private, parent_ca_name]) + have_private = ( + 'Yes' + if 'private' in cert_dict and 'key' in cert_dict['private'] + else 'No' + ) + data.append( + [ + cert_name, + cert.subject.rfc4514_string(), + cert_issuer_cn, + cert.not_valid_before, + cert.not_valid_after, + have_private, + parent_ca_name, + ] + ) - print("Certificate Authorities:") + print('Certificate Authorities:') print(tabulate.tabulate(data, headers)) @@ -1046,7 +1254,17 @@ def show_certificate( pem: typing.Optional[bool] = False, fingerprint: typing.Optional[ArgsFingerprint] = None, ): - headers = ['Name', 'Type', 'Subject CN', 'Issuer CN', 'Issued', 'Expiry', 'Revoked', 'Private Key', 'CA Present'] + headers = [ + 'Name', + 'Type', + 'Subject CN', + 'Issuer CN', + 'Issued', + 'Expiry', + 'Revoked', + 'Private Key', + 'CA Present', + ] data = [] certs = get_config_certificate() if certs: @@ -1071,8 +1289,8 @@ def show_certificate( return ca_name = get_certificate_ca(cert, ca_certs) - cert_subject_cn = cert.subject.rfc4514_string().split(",")[0] - cert_issuer_cn = cert.issuer.rfc4514_string().split(",")[0] + cert_subject_cn = cert.subject.rfc4514_string().split(',')[0] + cert_issuer_cn = cert.issuer.rfc4514_string().split(',')[0] cert_type = 'Unknown' try: @@ -1081,18 +1299,31 @@ def show_certificate( cert_type = 'Server' elif ext and ExtendedKeyUsageOID.CLIENT_AUTH in ext.value: cert_type = 'Client' - except: + except Exception: pass revoked = 'Yes' if 'revoke' in cert_dict else 'No' - have_private = 'Yes' if 'private' in cert_dict and 'key' in cert_dict['private'] else 'No' + have_private = ( + 'Yes' + if 'private' in cert_dict and 'key' in cert_dict['private'] + else 'No' + ) have_ca = f'Yes ({ca_name})' if ca_name else 'No' - data.append([ - cert_name, cert_type, cert_subject_cn, cert_issuer_cn, - cert.not_valid_before, cert.not_valid_after, - revoked, have_private, have_ca]) + data.append( + [ + cert_name, + cert_type, + cert_subject_cn, + cert_issuer_cn, + cert.not_valid_before, + cert.not_valid_after, + revoked, + have_private, + have_ca, + ] + ) - print("Certificates:") + print('Certificates:') print(tabulate.tabulate(data, headers)) @@ -1123,13 +1354,15 @@ def show_crl( print(encode_certificate(crl)) continue - certs = get_revoked_by_serial_numbers([revoked.serial_number for revoked in crl]) - data.append([cert_name, crl.last_update, ", ".join(certs)]) + certs = get_revoked_by_serial_numbers( + [revoked.serial_number for revoked in crl] + ) + data.append([cert_name, crl.last_update, ', '.join(certs)]) if name and pem: return - print("Certificate Revocation Lists:") + print('Certificate Revocation Lists:') print(tabulate.tabulate(data, headers)) -- cgit v1.2.3 From fa272f5297177354714ee7867104f43e4e322df6 Mon Sep 17 00:00:00 2001 From: Vijayakumar A <36878324+kumvijaya@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:45:22 +0530 Subject: T6784: enabled repo-sync wokrflow only for current and equuleus (#4163) --- .github/workflows/repo-sync.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/repo-sync.yml b/.github/workflows/repo-sync.yml index 6da2fb40d..752cf947a 100644 --- a/.github/workflows/repo-sync.yml +++ b/.github/workflows/repo-sync.yml @@ -6,6 +6,7 @@ on: - closed branches: - current + - equuleus workflow_dispatch: jobs: -- cgit v1.2.3