From 1607eec32641ad93ea211e447336b3366c28de06 Mon Sep 17 00:00:00 2001 From: zsdc Date: Fri, 13 Nov 2020 23:20:41 +0200 Subject: User-Data: T2116: Added module to apply config commands at deployment With the new `cc_vyos_userdata.py` module is possible to set in User-Data (`#cloud-config`) new parameter `vyos_config_commands`. This parameter should be a list of VyOS configuration commands that will be applied during deployment. The module will run after the Meta-Data module `cc_vyos.py`. Commands requirements: - one command per line - if command ending by value, it must be inside single quotes: `set some option 'value'`, `delete some option 'value'` - a single-quote symbol is not allowed inside command or value The commands list produced by the `show configuration commands` command on a VyOS router should comply with all the requirements, so it is easy to get a proper commands list by copying it from another router. Usage example (User-Data content): ``` #cloud-config vyos_config_commands: - set system host-name 'demo123' - set system ntp server 1.pool.ntp.org - set system ntp server 2.pool.ntp.org - delete interfaces ethernet eth2 address - set interfaces ethernet eth2 address '192.0.2.1/24' ``` --- cloudinit/config/cc_vyos_userdata.py | 213 +++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 cloudinit/config/cc_vyos_userdata.py (limited to 'cloudinit') diff --git a/cloudinit/config/cc_vyos_userdata.py b/cloudinit/config/cc_vyos_userdata.py new file mode 100644 index 00000000..52313433 --- /dev/null +++ b/cloudinit/config/cc_vyos_userdata.py @@ -0,0 +1,213 @@ +# 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)) -- cgit v1.2.3