# vi: ts=4 expandtab # # Copyright (C) 2020 Sentrium S.L. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import re from pathlib import Path from cloudinit import log as logging from cloudinit.settings import PER_INSTANCE from vyos.configtree import ConfigTree # configure logging logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) frequency = PER_INSTANCE # path to templates directory, required for analyzing nodes TEMPLATES_DIR = '/opt/vyatta/share/vyatta-cfg/templates/' # VyOS configuration files CFG_FILE_MAIN = '/opt/vyatta/etc/config/config.boot' CFG_FILE_DEFAULT = '/opt/vyatta/etc/config.boot.default' # get list of all tag nodes def get_tag_nodes(): try: logger.debug("Searching for tag nodes in configuration templates") tag_nodes = [] # search for node.tag directories node_tag_dirs = Path(TEMPLATES_DIR).rglob('node.tag') # add each found directory to tag nodes list for node_tag in node_tag_dirs: current_node_path = node_tag.relative_to(TEMPLATES_DIR).parent.parts tag_nodes.append(current_node_path) logger.debug("Tag nodes: {}".format(tag_nodes)) return tag_nodes except Exception as err: logger.error("Failed to find tag nodes: {}".format(err)) # get list of all multi nodes def get_multi_nodes(): try: logger.debug("Searching for multi nodes in configuration templates") multi_nodes = [] # search for node.def files node_def_files = Path(TEMPLATES_DIR).rglob('node.def') # prepare filter to match multi node files regex_filter = re.compile(r'^multi:$', re.MULTILINE) # add each node.def with multi mark to list for node_def_file in node_def_files: file_content = node_def_file.read_text() if regex_filter.search(file_content): current_multi_path = node_def_file.relative_to( TEMPLATES_DIR).parent.parts multi_nodes.append(current_multi_path) logger.debug("Multi nodes: {}".format(multi_nodes)) return multi_nodes except Exception as err: logger.error("Failed to find multi nodes: {}".format(err)) # check if a node is inside a list of nodes def inside_nodes_list(node_path, nodes_list): match = False # compare with all items in list for list_item in nodes_list: # continue only if lengths are equal if len(list_item) == len(node_path): # match parts of nodes paths one by one for element_id in list(range(len(node_path))): # break is items does not match if not (node_path[element_id] == list_item[element_id] or list_item[element_id] == 'node.tag'): break # match as tag node only if both nodes have the same length elif ((node_path[element_id] == list_item[element_id] or list_item[element_id] == 'node.tag') and element_id == len(node_path) - 1): match = True # break if we have a match if match is True: break return match # convert string to command (action + path + value) def string_to_command(stringcmd): # regex to split string to action + path + value regex_filter = re.compile( r'^(?Pset|delete) (?P[^\']+)( \'(?P.*)\')*$' ) if regex_filter.search(stringcmd): # command structure command = { 'cmd_action': regex_filter.search(stringcmd).group('cmd_action'), 'cmd_path': regex_filter.search(stringcmd).group('cmd_path').split(), 'cmd_value': regex_filter.search(stringcmd).group('cmd_value') } return command else: return None # helper: mark nodes as tag in config, if this is necessary def mark_tag(config, node_path, tag_nodes): current_node_path = [] # check and mark each element in command path if necessary for current_node in node_path: current_node_path.append(current_node) if inside_nodes_list(current_node_path, tag_nodes): logger.debug( "Marking node as tag: \"{}\"".format(current_node_path)) config.set_tag(current_node_path) # apply "set" command def apply_command_set(config, tag_nodes, multi_nodes, command): # if a node is multi type add value instead replacing replace_option = not inside_nodes_list(command['cmd_path'], multi_nodes) if not replace_option: logger.debug("{} is a multi node, adding value".format( command['cmd_path'])) config.set(command['cmd_path'], command['cmd_value'], replace=replace_option) # mark configured nodes as tag, if this is necessary mark_tag(config, command['cmd_path'], tag_nodes) # apply "delete" command def apply_command_delete(config, command): # delete a value if command['cmd_value']: config.delete_value(command['cmd_path'], command['cmd_value']) # otherwise delete path else: config.delete(command['cmd_path']) # apply command def apply_commands(config, commands_list): # get all tag and multi nodes tag_nodes = get_tag_nodes() multi_nodes = get_multi_nodes() # roll through configration commands for command_line in commands_list: # convert command to format, appliable to configuration command = string_to_command(command_line) # if conversion is successful, apply the command if command: logger.debug("Configuring command: \"{}\"".format(command_line)) try: if command['cmd_action'] == 'set': apply_command_set(config, tag_nodes, multi_nodes, command) if command['cmd_action'] == 'delete': apply_command_delete(config, command) except Exception as err: logger.error("Unable to configure command: {}".format(err)) # main config handler def handle(name, cfg, cloud, log, _args): # Get commands list to configure commands_list = cfg.get('vyos_config_commands', []) logger.debug("Commands to configure: {}".format(commands_list)) if commands_list: # open configuration file if Path(CFG_FILE_MAIN).exists(): config_file_path = CFG_FILE_MAIN else: config_file_path = CFG_FILE_DEFAULT logger.debug("Using configuration file: {}".format(config_file_path)) with open(config_file_path, 'r') as f: config_file = f.read() # load a file content into a config object config = ConfigTree(config_file) # Add configuration from the vyos_config_commands cloud-config section try: apply_commands(config, commands_list) except Exception as err: logger.error( "Failed to apply configuration commands: {}".format(err)) # save a new configuration file try: with open(config_file_path, 'w') as f: f.write(config.to_string()) logger.debug( "Configuration file saved: {}".format(config_file_path)) except Exception as err: logger.error("Failed to write config into the file {}: {}".format( config_file_path, err))