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/configsession.py | 78 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 18 deletions(-) (limited to 'python/vyos/configsession.py') 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) -- 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(-) (limited to 'python/vyos/configsession.py') 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 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(-) (limited to 'python/vyos/configsession.py') 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