diff options
Diffstat (limited to 'src')
| -rwxr-xr-x | src/conf_mode/load-balancing_reverse-proxy.py | 32 | ||||
| -rwxr-xr-x | src/conf_mode/protocols_bgp.py | 8 | ||||
| -rwxr-xr-x | src/conf_mode/service_dhcp-server.py | 2 | ||||
| -rwxr-xr-x | src/conf_mode/system_host-name.py | 6 | ||||
| -rw-r--r-- | src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf | 16 | ||||
| -rwxr-xr-x | src/helpers/vyos_config_sync.py | 66 | ||||
| -rwxr-xr-x | src/op_mode/image_manager.py | 44 | ||||
| -rwxr-xr-x | src/services/vyos-http-api-server | 38 | 
8 files changed, 129 insertions, 83 deletions
| diff --git a/src/conf_mode/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_reverse-proxy.py index 7338fe573..2a0acd84a 100755 --- a/src/conf_mode/load-balancing_reverse-proxy.py +++ b/src/conf_mode/load-balancing_reverse-proxy.py @@ -55,6 +55,29 @@ def get_config(config=None):      return lb +def _verify_cert(lb: dict, config: dict) -> None: +    if 'ca_certificate' in config['ssl']: +        ca_name = config['ssl']['ca_certificate'] +        pki_ca = lb['pki'].get('ca') +        if pki_ca is None: +            raise ConfigError(f'CA certificates does not exist in PKI') +        else: +            ca = pki_ca.get(ca_name) +            if ca is None: +                raise ConfigError(f'CA certificate "{ca_name}" does not exist') + +    elif 'certificate' in config['ssl']: +        cert_names = config['ssl']['certificate'] +        pki_certs = lb['pki'].get('certificate') +        if pki_certs is None: +            raise ConfigError(f'Certificates does not exist in PKI') + +        for cert_name in cert_names: +            pki_cert = pki_certs.get(cert_name) +            if pki_cert is None: +                raise ConfigError(f'Certificate "{cert_name}" does not exist') + +  def verify(lb):      if not lb:          return None @@ -83,6 +106,15 @@ def verify(lb):              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}"') +    for front, front_config in lb['service'].items(): +        if 'ssl' in front_config: +            _verify_cert(lb, front_config) + +    for back, back_config in lb['backend'].items(): +        if 'ssl' in back_config: +            _verify_cert(lb, back_config) + +  def generate(lb):      if not lb:          # Delete /run/haproxy/haproxy.cfg diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index f1c59cbde..512fa26e9 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -450,15 +450,15 @@ def verify(bgp):                              verify_route_map(afi_config['route_map'][tmp], bgp)                  if 'route_reflector_client' in afi_config: -                    if 'remote_as' in peer_config and peer_config['remote_as'] != 'internal' and peer_config['remote_as'] != bgp['system_as']: +                    peer_group_as = peer_config.get('remote_as') + +                    if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']):                          raise ConfigError('route-reflector-client only supported for iBGP peers')                      else:                          if 'peer_group' in peer_config:                              peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', bgp) -                            if peer_group_as != None and peer_group_as != 'internal' and peer_group_as != bgp['system_as']: +                            if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']):                                  raise ConfigError('route-reflector-client only supported for iBGP peers') -                        else: -                            raise ConfigError('route-reflector-client only supported for iBGP peers')      # Throw an error if a peer group is not configured for allow range      for prefix in dict_search('listen.range', bgp) or []: diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py index ba3d69b07..bf4454fda 100755 --- a/src/conf_mode/service_dhcp-server.py +++ b/src/conf_mode/service_dhcp-server.py @@ -316,7 +316,7 @@ def verify(dhcp):                  raise ConfigError(f'Invalid CA certificate specified for DHCP high-availability')      for address in (dict_search('listen_address', dhcp) or []): -        if is_addr_assigned(address): +        if is_addr_assigned(address, include_vrf=True):              listen_ok = True              # no need to probe further networks, we have one that is valid              continue diff --git a/src/conf_mode/system_host-name.py b/src/conf_mode/system_host-name.py index 6204cf247..8975cadb6 100755 --- a/src/conf_mode/system_host-name.py +++ b/src/conf_mode/system_host-name.py @@ -71,9 +71,9 @@ def get_config(config=None):                  hosts['nameserver'].append(ns)              else:                  tmp = '' -                if_type = Section.section(ns) -                if conf.exists(['interfaces', if_type, ns, 'address']): -                    tmp = conf.return_values(['interfaces', if_type, ns, 'address']) +                config_path = Section.get_config_path(ns) +                if conf.exists(['interfaces', config_path, 'address']): +                    tmp = conf.return_values(['interfaces', config_path, 'address'])                  hosts['nameservers_dhcp_interfaces'].update({ ns : tmp }) diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf index 518abeaec..9a8a53bfd 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf +++ b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf @@ -14,14 +14,6 @@ if /usr/bin/systemctl -q is-active vyos-hostsd; then              hostsd_changes=y          fi -        if [ -n "$new_dhcp6_domain_search" ]; then -            logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client" -            $hostsd_client --delete-search-domains --tag "dhcpv6-$interface" -            logmsg info "Adding search domain \"$new_dhcp6_domain_search\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" -            $hostsd_client --add-search-domains "$new_dhcp6_domain_search" --tag "dhcpv6-$interface" -            hostsd_changes=y -        fi -          if [ -n "$new_domain_name_servers" ]; then              logmsg info "Deleting nameservers with tag \"dhcp-$interface\" via vyos-hostsd-client"              $hostsd_client --delete-name-servers --tag "dhcp-$interface" @@ -30,14 +22,6 @@ if /usr/bin/systemctl -q is-active vyos-hostsd; then              hostsd_changes=y          fi -        if [ -n "$new_dhcp6_name_servers" ]; then -            logmsg info "Deleting nameservers with tag \"dhcpv6-$interface\" via vyos-hostsd-client" -            $hostsd_client --delete-name-servers --tag "dhcpv6-$interface" -            logmsg info "Adding nameservers \"$new_dhcp6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" -            $hostsd_client --add-name-servers $new_dhcp6_name_servers --tag "dhcpv6-$interface" -            hostsd_changes=y -        fi -          if [ $hostsd_changes ]; then              logmsg info "Applying changes via vyos-hostsd-client"              $hostsd_client --apply diff --git a/src/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py index 77f7cd810..0604b2837 100755 --- a/src/helpers/vyos_config_sync.py +++ b/src/helpers/vyos_config_sync.py @@ -21,9 +21,11 @@ import json  import requests  import urllib3  import logging -from typing import Optional, List, Union, Dict, Any +from typing import Optional, List, Tuple, Dict, Any  from vyos.config import Config +from vyos.configtree import ConfigTree +from vyos.configtree import mask_inclusive  from vyos.template import bracketize_ipv6 @@ -61,39 +63,45 @@ def post_request(url: str, -def retrieve_config(section: Optional[List[str]] = None) -> Optional[Dict[str, Any]]: +def retrieve_config(sections: List[list[str]]) -> Tuple[Dict[str, Any], Dict[str, Any]]:      """Retrieves the configuration from the local server.      Args: -        section: List[str]: The section of the configuration to retrieve. -        Default is None. +        sections: List[list[str]]: The list of sections of the configuration +        to retrieve, given as list of paths.      Returns: -        Optional[Dict[str, Any]]: The retrieved configuration as a -        dictionary, or None if an error occurred. +        Tuple[Dict[str, Any],Dict[str,Any]]: The tuple (mask, config) where: +            - mask: The tree of paths of sections, as a dictionary. +            - config: The subtree of masked config data, as a dictionary.      """ -    if section is None: -        section = [] -    conf = Config() -    config = conf.get_config_dict(section, get_first_key=True) -    if config: -        return config -    return None +    mask = ConfigTree('') +    for section in sections: +        mask.set(section) +    mask_dict = json.loads(mask.to_json()) + +    config = Config() +    config_tree = config.get_config_tree() +    masked = mask_inclusive(config_tree, mask) +    config_dict = json.loads(masked.to_json()) +    return mask_dict, config_dict  def set_remote_config(          address: str,          key: str, -        commands: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: +        op: str, +        mask: Dict[str, Any], +        config: Dict[str, Any]) -> Optional[Dict[str, Any]]:      """Loads the VyOS configuration in JSON format to a remote host.      Args:          address (str): The address of the remote host.          key (str): The key to use for loading the configuration. -        commands (list): List of set/load commands for request, given as: -                         [{'op': str, 'path': list[str], 'section': dict}, -                         ...] +        op (str): The operation to perform (set or load). +        mask (dict): The dict of paths in sections. +        config (dict): The dict of masked config data.      Returns:          Optional[Dict[str, Any]]: The response from the remote host as a @@ -107,7 +115,9 @@ def set_remote_config(      url = f'https://{address}/configure-section'      data = json.dumps({ -        'commands': commands, +        'op': op, +        'mask': mask, +        'config': config,          'key': key      }) @@ -140,23 +150,15 @@ def config_sync(secondary_address: str,      )      # Sync sections ("nat", "firewall", etc) -    commands = [] -    for section in sections: -        config_json = retrieve_config(section=section) -        # Check if config path deesn't exist, for example "set nat" -        # we set empty value for config_json data -        # As we cannot send to the remote host section "nat None" config -        if not config_json: -            config_json = {} -        logger.debug( -            f"Retrieved config for section '{section}': {config_json}") - -        d = {'op': mode, 'path': section, 'section': config_json} -        commands.append(d) +    mask_dict, config_dict = retrieve_config(sections) +    logger.debug( +        f"Retrieved config for sections '{sections}': {config_dict}")      set_config = set_remote_config(address=secondary_address,                                     key=secondary_key, -                                   commands=commands) +                                   op=mode, +                                   mask=mask_dict, +                                   config=config_dict)      logger.debug(f"Set config for sections '{sections}': {set_config}") diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py index 1510a667c..1cfb5f5a1 100755 --- a/src/op_mode/image_manager.py +++ b/src/op_mode/image_manager.py @@ -33,27 +33,31 @@ 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]: +def annotate_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 +        dict[str, str]: a dict of annotations indexed by image name      """ -    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 +    running = image.get_running_image() +    default = image.get_default_image() +    annotated = {} +    for image_name in images_list: +        annotated[image_name] = f'{image_name}' +    if running in images_list: +        annotated[running] = annotated[running] + ' (running)' +    if default in images_list: +        annotated[default] = annotated[default] + ' (default boot)' +    return annotated + +def define_format(images): +    annotated = annotate_list(images) +    def format_selection(image_name): +        return annotated[image_name] +    return format_selection  @compat.grub_cfg_update  def delete_image(image_name: Optional[str] = None, @@ -63,14 +67,16 @@ def delete_image(image_name: Optional[str] = None,      Args:          image_name (str): a name of image to delete      """ -    available_images: list[str] = annotated_list(grub.version_list()) +    available_images: list[str] = grub.version_list() +    format_selection = define_format(available_images)      if image_name is None:          if no_prompt:              exit('An image name is required for delete action')          else:              image_name = select_entry(available_images,                                        DELETE_IMAGE_LIST_MSG, -                                      DELETE_IMAGE_PROMPT_MSG) +                                      DELETE_IMAGE_PROMPT_MSG, +                                      format_selection)      if image_name == image.get_running_image():          exit(MSG_DELETE_IMAGE_RUNNING)      if image_name == image.get_default_image(): @@ -113,14 +119,16 @@ def set_image(image_name: Optional[str] = None,      Args:          image_name (str): an image name      """ -    available_images: list[str] = annotated_list(grub.version_list()) +    available_images: list[str] = grub.version_list() +    format_selection = define_format(available_images)      if image_name is None:          if not prompt:              exit('An image name is required for set action')          else:              image_name = select_entry(available_images,                                        SET_IMAGE_LIST_MSG, -                                      SET_IMAGE_PROMPT_MSG) +                                      SET_IMAGE_PROMPT_MSG, +                                      format_selection)      if image_name == image.get_default_image():          exit(f'The image "{image_name}" already configured as default')      if image_name not in available_images: diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 77870a84c..ecbf6fcf9 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -140,6 +140,14 @@ class ConfigSectionModel(ApiModel, BaseConfigSectionModel):  class ConfigSectionListModel(ApiModel):      commands: List[BaseConfigSectionModel] +class BaseConfigSectionTreeModel(BaseModel): +    op: StrictStr +    mask: Dict +    config: Dict + +class ConfigSectionTreeModel(ApiModel, BaseConfigSectionTreeModel): +    pass +  class RetrieveModel(ApiModel):      op: StrictStr      path: List[StrictStr] @@ -374,7 +382,7 @@ class MultipartRequest(Request):                          self.form_err = (400,                          f"Malformed command '{c}': missing 'op' field")                      if endpoint not in ('/config-file', '/container-image', -                                        '/image'): +                                        '/image', '/configure-section'):                          if 'path' not in c:                              self.form_err = (400,                              f"Malformed command '{c}': missing 'path' field") @@ -392,12 +400,9 @@ class MultipartRequest(Request):                              self.form_err = (400,                              f"Malformed command '{c}': 'value' field must be a string")                      if endpoint in ('/configure-section'): -                        if 'section' not in c: -                            self.form_err = (400, -                            f"Malformed command '{c}': missing 'section' field") -                        elif not isinstance(c['section'], dict): +                        if 'section' not in c and 'config' not in c:                              self.form_err = (400, -                            f"Malformed command '{c}': 'section' field must be JSON of dict") +                            f"Malformed command '{c}': missing 'section' or 'config' field")                  if 'key' not in forms and 'key' not in merge:                      self.form_err = (401, "Valid API key is required") @@ -455,7 +460,8 @@ def call_commit(s: ConfigSession):              logger.warning(f"ConfigSessionError: {e}")  def _configure_op(data: Union[ConfigureModel, ConfigureListModel, -                              ConfigSectionModel, ConfigSectionListModel], +                              ConfigSectionModel, ConfigSectionListModel, +                              ConfigSectionTreeModel],                    request: Request, background_tasks: BackgroundTasks):      session = app.state.vyos_session      env = session.get_session_env() @@ -481,7 +487,8 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,      try:          for c in data:              op = c.op -            path = c.path +            if not isinstance(c, BaseConfigSectionTreeModel): +                path = c.path              if isinstance(c, BaseConfigureModel):                  if c.value: @@ -495,6 +502,10 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,              elif isinstance(c, BaseConfigSectionModel):                  section = c.section +            elif isinstance(c, BaseConfigSectionTreeModel): +                mask = c.mask +                config = c.config +              if isinstance(c, BaseConfigureModel):                  if op == 'set':                      session.set(path, value=value) @@ -514,6 +525,14 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,                      session.load_section(path, section)                  else:                      raise ConfigSessionError(f"'{op}' is not a valid operation") + +            elif isinstance(c, BaseConfigSectionTreeModel): +                if op == 'set': +                    session.set_section_tree(config) +                elif op == 'load': +                    session.load_section_tree(mask, config) +                else: +                    raise ConfigSessionError(f"'{op}' is not a valid operation")          # end for          config = Config(session_env=env)          d = get_config_diff(config) @@ -554,7 +573,8 @@ def configure_op(data: Union[ConfigureModel,  @app.post('/configure-section')  def configure_section_op(data: Union[ConfigSectionModel, -                                     ConfigSectionListModel], +                                     ConfigSectionListModel, +                                     ConfigSectionTreeModel],                                 request: Request, background_tasks: BackgroundTasks):      return _configure_op(data, request, background_tasks) | 
