diff options
Diffstat (limited to 'src')
| -rwxr-xr-x | src/conf_mode/pki.py | 40 | ||||
| -rwxr-xr-x | src/conf_mode/protocols_isis.py | 15 | ||||
| -rwxr-xr-x | src/conf_mode/protocols_ospf.py | 13 | ||||
| -rwxr-xr-x | src/conf_mode/service_https.py | 237 | ||||
| -rwxr-xr-x | src/conf_mode/vpn_ipsec.py | 11 | ||||
| -rw-r--r-- | src/etc/systemd/system/nginx.service.d/10-override.conf | 3 | ||||
| -rwxr-xr-x | src/helpers/vyos-boot-config-loader.py | 3 | ||||
| -rwxr-xr-x | src/migration-scripts/https/5-to-6 | 76 | ||||
| -rwxr-xr-x | src/op_mode/image_manager.py | 25 | ||||
| -rwxr-xr-x | src/services/vyos-http-api-server | 10 | 
10 files changed, 240 insertions, 193 deletions
| diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index 310519abd..4be40e99e 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -104,10 +104,10 @@ def certbot_request(name: str, config: dict, dry_run: bool=True):          return      domains = '--domains ' + ' --domains '.join(config['domain_name']) -    tmp = f'certbot certonly --config-dir {vyos_certbot_dir} --cert-name {name} '\ -            f'--non-interactive --standalone --agree-tos --no-eff-email --expand '\ -            f'--server {config["url"]} --email {config["email"]} '\ -            f'--key-type rsa --rsa-key-size {config["rsa_key_size"]} {domains}' +    tmp = f'certbot certonly --non-interactive --config-dir {vyos_certbot_dir} --cert-name {name} '\ +          f'--standalone --agree-tos --no-eff-email --expand --server {config["url"]} '\ +          f'--email {config["email"]} --key-type rsa --rsa-key-size {config["rsa_key_size"]} '\ +          f'{domains}'      if 'listen_address' in config:          tmp += f' --http-01-address {config["listen_address"]}'      # verify() does not need to actually request a cert but only test for plausability @@ -130,29 +130,27 @@ def get_config(config=None):      if len(argv) > 1 and argv[1] == 'certbot_renew':          pki['certbot_renew'] = {} -    tmp = node_changed(conf, base + ['ca'], key_mangling=('-', '_'), recursive=True) +    tmp = node_changed(conf, base + ['ca'], recursive=True)      if tmp:          if 'changed' not in pki: pki.update({'changed':{}})          pki['changed'].update({'ca' : tmp}) -    tmp = node_changed(conf, base + ['certificate'], key_mangling=('-', '_'), -                       recursive=True, expand_nodes=Diff.ADD|Diff.DELETE) +    tmp = node_changed(conf, base + ['certificate'], recursive=True)      if tmp:          if 'changed' not in pki: pki.update({'changed':{}})          pki['changed'].update({'certificate' : tmp}) -    tmp = node_changed(conf, base + ['dh'], key_mangling=('-', '_'), recursive=True) +    tmp = node_changed(conf, base + ['dh'], recursive=True)      if tmp:          if 'changed' not in pki: pki.update({'changed':{}})          pki['changed'].update({'dh' : tmp}) -    tmp = node_changed(conf, base + ['key-pair'], key_mangling=('-', '_'), recursive=True) +    tmp = node_changed(conf, base + ['key-pair'], recursive=True)      if tmp:          if 'changed' not in pki: pki.update({'changed':{}})          pki['changed'].update({'key_pair' : tmp}) -    tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], key_mangling=('-', '_'), -                       recursive=True) +    tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], recursive=True)      if tmp:          if 'changed' not in pki: pki.update({'changed':{}})          pki['changed'].update({'openvpn' : tmp}) @@ -211,7 +209,7 @@ def get_config(config=None):                          if found_name == item_name:                              path = search['path']                              path_str = ' '.join(path + found_path) -                            print(f'pki: Updating config: {path_str} {found_name}') +                            print(f'PKI: Updating config: {path_str} {found_name}')                              if path[0] == 'interfaces':                                  ifname = found_path[0] @@ -371,21 +369,29 @@ def generate(pki):      if 'certbot_renew' in pki:          return None -    # list of certificates issued via certbot      certbot_list = [] +    certbot_list_on_disk = [] +    if os.path.exists(f'{vyos_certbot_dir}/live'): +        certbot_list_on_disk = [f.path.split('/')[-1] for f in os.scandir(f'{vyos_certbot_dir}/live') if f.is_dir()] +      if 'certificate' in pki: +        changed_certificates = dict_search('changed.certificate', pki)          for name, cert_conf in pki['certificate'].items():              if 'acme' in cert_conf:                  certbot_list.append(name) -                # when something for the certificate changed, we should delete it -                if name in dict_search('changed.certificate', pki): -                    certbot_delete(name) +                # generate certificate if not found on disk +                if name not in certbot_list_on_disk: +                    certbot_request(name, cert_conf['acme'], dry_run=False) +                elif changed_certificates != None and name in changed_certificates: +                    # when something for the certificate changed, we should delete it +                    if name in certbot_list_on_disk: +                        certbot_delete(name)                      certbot_request(name, cert_conf['acme'], dry_run=False)      # Cleanup certbot configuration and certificates if no longer in use by CLI      # Get foldernames under vyos_certbot_dir which each represent a certbot cert      if os.path.exists(f'{vyos_certbot_dir}/live'): -        for cert in [f.path.split('/')[-1] for f in os.scandir(f'{vyos_certbot_dir}/live') if f.is_dir()]: +        for cert in certbot_list_on_disk:              if cert not in certbot_list:                  # certificate is no longer active on the CLI - remove it                  certbot_delete(cert) diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index ce67ccff7..8d594bb68 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -220,7 +220,20 @@ def verify(isis):                  if ("explicit_null" in prefix_config['index']) and ("no_php_flag" in prefix_config['index']):                      raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\                                        f'and no-php-flag configured at the same time.') -                 +  +    # Check for index ranges being larger than the segment routing global block     +    if dict_search('segment_routing.global_block', isis): +        g_high_label_value = dict_search('segment_routing.global_block.high_label_value', isis) +        g_low_label_value = dict_search('segment_routing.global_block.low_label_value', isis) +        g_label_difference = int(g_high_label_value) - int(g_low_label_value) +        if dict_search('segment_routing.prefix', isis): +            for prefix, prefix_config in isis['segment_routing']['prefix'].items(): +                if 'index' in prefix_config: +                    index_size = isis['segment_routing']['prefix'][prefix]['index']['value'] +                    if int(index_size) > int(g_label_difference): +                        raise ConfigError(f'Segment routing prefix {prefix} cannot have an '\ +                                          f'index base size larger than the SRGB label base.') +                  # Check for LFA tiebreaker index duplication      if dict_search('fast_reroute.lfa.local.tiebreaker', isis):          comparison_dictionary = {} diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index 2f07142a3..34cf49286 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -213,6 +213,19 @@ def verify(ospf):                      raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\                                        f'and no-php-flag configured at the same time.') +    # Check for index ranges being larger than the segment routing global block     +    if dict_search('segment_routing.global_block', ospf): +        g_high_label_value = dict_search('segment_routing.global_block.high_label_value', ospf) +        g_low_label_value = dict_search('segment_routing.global_block.low_label_value', ospf) +        g_label_difference = int(g_high_label_value) - int(g_low_label_value) +        if dict_search('segment_routing.prefix', ospf): +            for prefix, prefix_config in ospf['segment_routing']['prefix'].items(): +                if 'index' in prefix_config: +                    index_size = ospf['segment_routing']['prefix'][prefix]['index']['value'] +                    if int(index_size) > int(g_label_difference): +                        raise ConfigError(f'Segment routing prefix {prefix} cannot have an '\ +                                          f'index base size larger than the SRGB label base.') +      # Check route summarisation      if 'summary_address' in ospf:          for prefix, prefix_options in ospf['summary_address'].items(): diff --git a/src/conf_mode/service_https.py b/src/conf_mode/service_https.py index 2e7ebda5a..46efc3c93 100755 --- a/src/conf_mode/service_https.py +++ b/src/conf_mode/service_https.py @@ -15,51 +15,41 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import os +import socket  import sys  import json -from copy import deepcopy  from time import sleep -import vyos.defaults -  from vyos.base import Warning  from vyos.config import Config +from vyos.config import config_dict_merge  from vyos.configdiff import get_config_diff  from vyos.configverify import verify_vrf -from vyos import ConfigError +from vyos.defaults import api_config_state  from vyos.pki import wrap_certificate  from vyos.pki import wrap_private_key +from vyos.pki import wrap_dh_parameters +from vyos.pki import load_dh_parameters  from vyos.template import render +from vyos.utils.dict import dict_search  from vyos.utils.process import call +from vyos.utils.process import is_systemd_service_active  from vyos.utils.network import check_port_availability  from vyos.utils.network import is_listen_port_bind_service  from vyos.utils.file import write_file - +from vyos import ConfigError  from vyos import airbag  airbag.enable() -config_file = '/etc/nginx/sites-available/default' +config_file = '/etc/nginx/sites-enabled/default'  systemd_override = r'/run/systemd/system/nginx.service.d/override.conf' -cert_dir = '/etc/ssl/certs' -key_dir = '/etc/ssl/private' - -api_config_state = '/run/http-api-state' -systemd_service = '/run/systemd/system/vyos-http-api.service' - -# https config needs to coordinate several subsystems: api, -# self-signed certificate, as well as the virtual hosts defined within the -# https config definition itself. Consequently, one needs a general dict, -# encompassing the https and other configs, and a list of such virtual hosts -# (server blocks in nginx terminology) to pass to the jinja2 template. -default_server_block = { -    'id'        : '', -    'address'   : '*', -    'port'      : '443', -    'name'      : ['_'], -    'api'       : False, -    'vyos_cert' : {}, -} +cert_dir = '/run/nginx/certs' + +user = 'www-data' +group = 'www-data' + +systemd_service_api = '/run/systemd/system/vyos-http-api.service'  def get_config(config=None):      if config: @@ -71,83 +61,70 @@ def get_config(config=None):      if not conf.exists(base):          return None -    diff = get_config_diff(conf) - -    https = conf.get_config_dict(base, get_first_key=True, with_pki=True) +    https = conf.get_config_dict(base, get_first_key=True, +                                 key_mangling=('-', '_'), +                                 with_pki=True) -    https['api_add_or_delete'] = diff.node_changed_presence(base + ['api']) +    # store path to API config file for later use in templates +    https['api_config_state'] = api_config_state +    # get fully qualified system hsotname +    https['hostname'] = socket.getfqdn() -    if 'api' not in https: -        return https +    # We have gathered the dict representation of the CLI, but there are default +    # options which we need to update into the dictionary retrived. +    default_values = conf.get_config_defaults(**https.kwargs, recursive=True) +    if 'api' not in https or 'graphql' not in https['api']: +        del default_values['api'] -    http_api = conf.get_config_dict(base + ['api'], key_mangling=('-', '_'), -                                    no_tag_node_value_mangle=True, -                                    get_first_key=True, -                                    with_recursive_defaults=True) - -    if http_api.from_defaults(['graphql']): -        del http_api['graphql'] - -    # Do we run inside a VRF context? -    vrf_path = ['service', 'https', 'vrf'] -    if conf.exists(vrf_path): -        http_api['vrf'] = conf.return_value(vrf_path) - -    https['api'] = http_api +    # merge CLI and default dictionary +    https = config_dict_merge(default_values, https)      return https  def verify(https): -    from vyos.utils.dict import dict_search -      if https is None:          return None -    if 'certificates' in https: -        certificates = https['certificates'] +    if 'certificates' in https and 'certificate' in https['certificates']: +        cert_name = https['certificates']['certificate'] +        if 'pki' not in https: +            raise ConfigError('PKI is not configured!') -        if 'certificate' in certificates: -            if not https['pki']: -                raise ConfigError('PKI is not configured') +        if cert_name not in https['pki']['certificate']: +            raise ConfigError('Invalid certificate in configuration!') -            cert_name = certificates['certificate'] +        pki_cert = https['pki']['certificate'][cert_name] -            if cert_name not in https['pki']['certificate']: -                raise ConfigError('Invalid certificate on https configuration') +        if 'certificate' not in pki_cert: +            raise ConfigError('Missing certificate in configuration!') -            pki_cert = https['pki']['certificate'][cert_name] +        if 'private' not in pki_cert or 'key' not in pki_cert['private']: +            raise ConfigError('Missing certificate private key in configuration!') -            if 'certificate' not in pki_cert: -                raise ConfigError('Missing certificate on https configuration') +        if 'dh_params' in https['certificates']: +            dh_name = https['certificates']['dh_params'] +            if dh_name not in https['pki']['dh']: +                raise ConfigError('Invalid DH parameter in configuration!') -            if 'private' not in pki_cert or 'key' not in pki_cert['private']: -                raise ConfigError("Missing certificate private key on https configuration") -    else: -        Warning('No certificate specified, using buildin self-signed certificates!') +            pki_dh = https['pki']['dh'][dh_name] +            dh_params = load_dh_parameters(pki_dh['parameters']) +            dh_numbers = dh_params.parameter_numbers() +            dh_bits = dh_numbers.p.bit_length() +            if dh_bits < 2048: +                raise ConfigError(f'Minimum DH key-size is 2048 bits') -    server_block_list = [] +    else: +        Warning('No certificate specified, using build-in self-signed certificates. '\ +                'Do not use them in a production environment!') -    # organize by vhosts -    vhost_dict = https.get('virtual-host', {}) +    # Check if server port is already in use by a different appliaction +    listen_address = ['0.0.0.0'] +    port = int(https['port']) +    if 'listen_address' in https: +        listen_address = https['listen_address'] -    if not vhost_dict: -        # no specified virtual hosts (server blocks); use default -        server_block_list.append(default_server_block) -    else: -        for vhost in list(vhost_dict): -            server_block = deepcopy(default_server_block) -            data = vhost_dict.get(vhost, {}) -            server_block['address'] = data.get('listen-address', '*') -            server_block['port'] = data.get('port', '443') -            server_block_list.append(server_block) - -    for entry in server_block_list: -        _address = entry.get('address') -        _address = '0.0.0.0' if _address == '*' else _address -        _port = entry.get('port') -        proto = 'tcp' -        if check_port_availability(_address, int(_port), proto) is not True and \ -                not is_listen_port_bind_service(int(_port), 'nginx'): -            raise ConfigError(f'"{proto}" port "{_port}" is used by another service') +    for address in listen_address: +        if not check_port_availability(address, port, 'tcp') and not is_listen_port_bind_service(port, 'nginx'): +            raise ConfigError(f'TCP port "{port}" is used by another service!')      verify_vrf(https) @@ -172,89 +149,61 @@ def verify(https):          # If only key-based methods are enabled,          # fail the commit if no valid key configurations are found          if (not valid_keys_exist) and (not jwt_auth): -            raise ConfigError('At least one HTTPS API key is required unless GraphQL token authentication is enabled') +            raise ConfigError('At least one HTTPS API key is required unless GraphQL token authentication is enabled!')          if (not valid_keys_exist) and jwt_auth: -            Warning(f'API keys are not configured: the classic (non-GraphQL) API will be unavailable.') +            Warning(f'API keys are not configured: classic (non-GraphQL) API will be unavailable!')      return None  def generate(https):      if https is None: +        for file in [systemd_service_api, config_file, systemd_override]: +            if os.path.exists(file): +                os.unlink(file)          return None -    if 'api' not in https: -        if os.path.exists(systemd_service): -            os.unlink(systemd_service) -    else: -        render(systemd_service, 'https/vyos-http-api.service.j2', https['api']) +    if 'api' in https: +        render(systemd_service_api, 'https/vyos-http-api.service.j2', https)          with open(api_config_state, 'w') as f:              json.dump(https['api'], f, indent=2) - -    server_block_list = [] - -    # organize by vhosts - -    vhost_dict = https.get('virtual-host', {}) - -    if not vhost_dict: -        # no specified virtual hosts (server blocks); use default -        server_block_list.append(default_server_block)      else: -        for vhost in list(vhost_dict): -            server_block = deepcopy(default_server_block) -            server_block['id'] = vhost -            data = vhost_dict.get(vhost, {}) -            server_block['address'] = data.get('listen-address', '*') -            server_block['port'] = data.get('port', '443') -            name = data.get('server-name', ['_']) -            server_block['name'] = name -            allow_client = data.get('allow-client', {}) -            server_block['allow_client'] = allow_client.get('address', []) -            server_block_list.append(server_block) +        if os.path.exists(systemd_service_api): +            os.unlink(systemd_service_api)      # get certificate data - -    cert_dict = https.get('certificates', {}) - -    if 'certificate' in cert_dict: -        cert_name = cert_dict['certificate'] +    if 'certificates' in https and 'certificate' in https['certificates']: +        cert_name = https['certificates']['certificate']          pki_cert = https['pki']['certificate'][cert_name] -        cert_path = os.path.join(cert_dir, f'{cert_name}.pem') -        key_path = os.path.join(key_dir, f'{cert_name}.pem') +        cert_path = os.path.join(cert_dir, f'{cert_name}_cert.pem') +        key_path = os.path.join(cert_dir, f'{cert_name}_key.pem')          server_cert = str(wrap_certificate(pki_cert['certificate'])) -        if 'ca-certificate' in cert_dict: -            ca_cert = cert_dict['ca-certificate'] -            server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate'])) -        write_file(cert_path, server_cert) -        write_file(key_path, wrap_private_key(pki_cert['private']['key'])) +        # Append CA certificate if specified to form a full chain +        if 'ca_certificate' in https['certificates']: +            ca_cert = https['certificates']['ca_certificate'] +            server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate'])) -        vyos_cert_data = { -            'crt': cert_path, -            'key': key_path -        } +        write_file(cert_path, server_cert, user=user, group=group, mode=0o644) +        write_file(key_path, wrap_private_key(pki_cert['private']['key']), +                    user=user, group=group, mode=0o600) -        for block in server_block_list: -            block['vyos_cert'] = vyos_cert_data +        tmp_path = {'cert_path': cert_path, 'key_path': key_path} -    if 'api' in list(https): -        vhost_list = https.get('api-restrict', {}).get('virtual-host', []) -        if not vhost_list: -            for block in server_block_list: -                block['api'] = True -        else: -            for block in server_block_list: -                if block['id'] in vhost_list: -                    block['api'] = True +        if 'dh_params' in https['certificates']: +            dh_name = https['certificates']['dh_params'] +            pki_dh = https['pki']['dh'][dh_name] +            if 'parameters' in pki_dh: +                dh_path = os.path.join(cert_dir, f'{dh_name}_dh.pem') +                write_file(dh_path, wrap_dh_parameters(pki_dh['parameters']), +                           user=user, group=group, mode=0o600) +                tmp_path.update({'dh_file' : dh_path}) -    data = { -        'server_block_list': server_block_list, -    } +        https['certificates'].update(tmp_path) -    render(config_file, 'https/nginx.default.j2', data) +    render(config_file, 'https/nginx.default.j2', https)      render(systemd_override, 'https/override.conf.j2', https)      return None @@ -273,7 +222,7 @@ def apply(https):          call(f'systemctl reload-or-restart {http_api_service_name}')          # Let uvicorn settle before (possibly) restarting nginx          sleep(1) -    else: +    elif is_systemd_service_active(http_api_service_name):          call(f'systemctl stop {http_api_service_name}')      call(f'systemctl reload-or-restart {https_service_name}') diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 7fd32c230..5bdcf2fa1 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -43,6 +43,7 @@ from vyos.template import is_ipv4  from vyos.template import is_ipv6  from vyos.template import render  from vyos.utils.network import is_ipv6_link_local +from vyos.utils.network import interface_exists  from vyos.utils.dict import dict_search  from vyos.utils.dict import dict_search_args  from vyos.utils.process import call @@ -65,11 +66,11 @@ default_install_routes = 'yes'  vici_socket = '/var/run/charon.vici' -CERT_PATH = f'{swanctl_dir}/x509/' +CERT_PATH   = f'{swanctl_dir}/x509/'  PUBKEY_PATH = f'{swanctl_dir}/pubkey/' -KEY_PATH  = f'{swanctl_dir}/private/' -CA_PATH   = f'{swanctl_dir}/x509ca/' -CRL_PATH  = f'{swanctl_dir}/x509crl/' +KEY_PATH    = f'{swanctl_dir}/private/' +CA_PATH     = f'{swanctl_dir}/x509ca/' +CRL_PATH    = f'{swanctl_dir}/x509crl/'  DHCP_HOOK_IFLIST = '/tmp/ipsec_dhcp_waiting' @@ -394,7 +395,7 @@ def verify(ipsec):                  if 'bind' in peer_conf['vti']:                      vti_interface = peer_conf['vti']['bind'] -                    if not os.path.exists(f'/sys/class/net/{vti_interface}'): +                    if not interface_exists(vti_interface):                          raise ConfigError(f'VTI interface {vti_interface} for site-to-site peer {peer} does not exist!')              if 'vti' not in peer_conf and 'tunnel' not in peer_conf: diff --git a/src/etc/systemd/system/nginx.service.d/10-override.conf b/src/etc/systemd/system/nginx.service.d/10-override.conf new file mode 100644 index 000000000..1be5cec81 --- /dev/null +++ b/src/etc/systemd/system/nginx.service.d/10-override.conf @@ -0,0 +1,3 @@ +[Unit] +After= +After=vyos-router.service diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py index 01b06526d..42de696ce 100755 --- a/src/helpers/vyos-boot-config-loader.py +++ b/src/helpers/vyos-boot-config-loader.py @@ -102,7 +102,8 @@ def failsafe(config_file_name):                                        'authentication',                                        'encrypted-password']) -    cmd(f"useradd -s /bin/bash -G 'users,sudo' -m -N -p '{passwd}' vyos") +    cmd(f"useradd --create-home --no-user-group --shell /bin/vbash --password '{passwd}' "\ +        "--groups frr,frrvty,vyattacfg,sudo,adm,dip,disk vyos")  if __name__ == '__main__':      if len(sys.argv) < 2: diff --git a/src/migration-scripts/https/5-to-6 b/src/migration-scripts/https/5-to-6 index b4159f02f..6d6efd32c 100755 --- a/src/migration-scripts/https/5-to-6 +++ b/src/migration-scripts/https/5-to-6 @@ -16,12 +16,14 @@  # T5886: Add support for ACME protocol (LetsEncrypt), migrate https certbot  #        to new "pki certificate" CLI tree +# T5902: Remove virtual-host  import os  import sys  from vyos.configtree import ConfigTree  from vyos.defaults import directories +from vyos.utils.process import cmd  vyos_certbot_dir = directories['certbot'] @@ -36,30 +38,68 @@ with open(file_name, 'r') as f:  config = ConfigTree(config_file) -base = ['service', 'https', 'certificates'] +base = ['service', 'https']  if not config.exists(base):      # Nothing to do      sys.exit(0) -# both domain-name and email must be set on CLI - ensured by previous verify() -domain_names = config.return_values(base + ['certbot', 'domain-name']) -email = config.return_value(base + ['certbot', 'email']) -config.delete(base) - -# Set default certname based on domain-name -cert_name = 'https-' + domain_names[0].split('.')[0] -# Overwrite certname from previous certbot calls if available -if os.path.exists(f'{vyos_certbot_dir}/live'): -    for cert in [f.path.split('/')[-1] for f in os.scandir(f'{vyos_certbot_dir}/live') if f.is_dir()]: -        cert_name = cert -        break - -for domain in domain_names: -    config.set(['pki', 'certificate', cert_name, 'acme', 'domain-name'], value=domain, replace=False) +if config.exists(base + ['certificates']): +    # both domain-name and email must be set on CLI - ensured by previous verify() +    domain_names = config.return_values(base + ['certificates', 'certbot', 'domain-name']) +    email = config.return_value(base + ['certificates', 'certbot', 'email']) +    config.delete(base + ['certificates']) + +    # Set default certname based on domain-name +    cert_name = 'https-' + domain_names[0].split('.')[0] +    # Overwrite certname from previous certbot calls if available +    # We can not use python code like os.scandir due to filesystem permissions. +    # This must be run as root +    certbot_live = f'{vyos_certbot_dir}/live/' # we need the trailing / +    if os.path.exists(certbot_live): +        tmp = cmd(f'sudo find {certbot_live} -maxdepth 1 -type d') +        tmp = tmp.split() # tmp = ['/config/auth/letsencrypt/live', '/config/auth/letsencrypt/live/router.vyos.net'] +        tmp.remove(certbot_live) +        cert_name = tmp[0].replace(certbot_live, '') +      config.set(['pki', 'certificate', cert_name, 'acme', 'email'], value=email) +    config.set_tag(['pki', 'certificate']) +    for domain in domain_names: +        config.set(['pki', 'certificate', cert_name, 'acme', 'domain-name'], value=domain, replace=False) + +    # Update Webserver certificate +    config.set(base + ['certificates', 'certificate'], value=cert_name) + +if config.exists(base + ['virtual-host']): +    allow_client = [] +    listen_port = [] +    listen_address = [] +    for virtual_host in config.list_nodes(base + ['virtual-host']): +        allow_path = base + ['virtual-host', virtual_host, 'allow-client', 'address'] +        if config.exists(allow_path): +            tmp = config.return_values(allow_path) +            allow_client.extend(tmp) + +        port_path = base + ['virtual-host', virtual_host, 'listen-port'] +        if config.exists(port_path): +            tmp = config.return_value(port_path) +            listen_port.append(tmp) + +        listen_address_path = base + ['virtual-host', virtual_host, 'listen-address'] +        if config.exists(listen_address_path): +            tmp = config.return_value(listen_address_path) +            listen_address.append(tmp) + +    config.delete(base + ['virtual-host']) +    for client in allow_client: +        config.set(base + ['allow-client', 'address'], value=client, replace=False) + +    #  clear listen-address if "all" were specified +    if '*' in listen_address: +        listen_address = [] +    for address in listen_address: +        config.set(base + ['listen-address'], value=address, replace=False) + -# Update Webserver certificate -config.set(base + ['certificate'], value=cert_name)  try:      with open(file_name, 'w') as f: diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py index e75485f9f..e64a85b95 100755 --- a/src/op_mode/image_manager.py +++ b/src/op_mode/image_manager.py @@ -33,6 +33,27 @@ DELETE_IMAGE_PROMPT_MSG: str = 'Select an image to delete:'  MSG_DELETE_IMAGE_RUNNING: str = 'Currently running image cannot be deleted; reboot into another image first'  MSG_DELETE_IMAGE_DEFAULT: str = 'Default image cannot be deleted; set another image as default first' +def annotated_list(images_list: list[str]) -> list[str]: +    """Annotate list of images with additional info + +    Args: +        images_list (list[str]): a list of image names + +    Returns: +        list[str]: a list of image names with additional info +    """ +    index_running: int = None +    index_default: int = None +    try: +        index_running = images_list.index(image.get_running_image()) +        index_default = images_list.index(image.get_default_image()) +    except ValueError: +        pass +    if index_running is not None: +        images_list[index_running] += ' (running)' +    if index_default is not None: +        images_list[index_default] += ' (default boot)' +    return images_list  @compat.grub_cfg_update  def delete_image(image_name: Optional[str] = None, @@ -42,7 +63,7 @@ def delete_image(image_name: Optional[str] = None,      Args:          image_name (str): a name of image to delete      """ -    available_images: list[str] = grub.version_list() +    available_images: list[str] = annotated_list(grub.version_list())      if image_name is None:          if no_prompt:              exit('An image name is required for delete action') @@ -83,7 +104,7 @@ def set_image(image_name: Optional[str] = None,      Args:          image_name (str): an image name      """ -    available_images: list[str] = grub.version_list() +    available_images: list[str] = annotated_list(grub.version_list())      if image_name is None:          if not prompt:              exit('An image name is required for set action') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index b64e58132..40d442e30 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -1,6 +1,6 @@  #!/usr/share/vyos-http-api-tools/bin/python3  # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-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 @@ -13,8 +13,6 @@  #  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -# -#  import os  import sys @@ -25,6 +23,7 @@ import logging  import signal  import traceback  import threading +  from time import sleep  from typing import List, Union, Callable, Dict @@ -46,11 +45,12 @@ from ariadne.asgi import GraphQL  from vyos.config import Config  from vyos.configtree import ConfigTree  from vyos.configdiff import get_config_diff -from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError +from vyos.defaults import api_config_state  import api.graphql.state -api_config_state = '/run/http-api-state'  CFG_GROUP = 'vyattacfg'  debug = True | 
