# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. """ Functions for reading/writing component versions. The config file version string has the following form: VyOS 1.3/1.4: // Warning: Do not remove the following line. // vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" // Release version: 1.3.0 VyOS 1.2: /* Warning: Do not remove the following line. */ /* === vyatta-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@2:dhcp-server@5:dns-forwarding@1:firewall@5:ipsec@5:l2tp@1:mdns@1:nat@4:ntp@1:pppoe-server@2:pptp@1:qos@1:quagga@7:snmp@1:ssh@1:system@10:vrrp@2:wanloadbalance@3:webgui@1:webproxy@2:zone-policy@1" === */ /* Release version: 1.2.8 */ """ import os import re import sys import fileinput from vyos.xml_ref import component_version from vyos.version import get_version from vyos.defaults import directories DEFAULT_CONFIG_PATH = os.path.join(directories['config'], 'config.boot') def from_string(string_line, vintage='vyos'): """ Get component version dictionary from string. Return empty dictionary if string contains no config information or raise error if component version string malformed. """ version_dict = {} if vintage == 'vyos': if re.match(r'// vyos-config-version:.+', string_line): if not re.match(r'// vyos-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s*', string_line): raise ValueError(f"malformed configuration string: {string_line}") for pair in re.findall(r'([\w,-]+)@(\d+)', string_line): version_dict[pair[0]] = int(pair[1]) elif vintage == 'vyatta': if re.match(r'/\* === vyatta-config-version:.+=== \*/$', string_line): if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', string_line): raise ValueError(f"malformed configuration string: {string_line}") for pair in re.findall(r'([\w,-]+)@(\d+)', string_line): version_dict[pair[0]] = int(pair[1]) else: raise ValueError("Unknown config string vintage") return version_dict def from_file(config_file_name=DEFAULT_CONFIG_PATH, vintage='vyos'): """ Get component version dictionary parsing config file line by line """ with open(config_file_name, 'r') as f: for line_in_config in f: version_dict = from_string(line_in_config, vintage=vintage) if version_dict: return version_dict # no version information return {} def from_system(): """ Get system component version dict. """ return component_version() def legacy_from_system(): """ Get system component version dict from legacy location. This is for a transitional sanity check; the directory will eventually be removed. """ system_versions = {} legacy_dir = directories['current'] # To be removed: if not os.path.isdir(legacy_dir): return system_versions try: version_info = os.listdir(legacy_dir) except OSError as err: sys.exit(repr(err)) for info in version_info: if re.match(r'[\w,-]+@\d+', info): pair = info.split('@') system_versions[pair[0]] = int(pair[1]) return system_versions def format_string(ver: dict) -> str: """ Version dict to string. """ keys = list(ver) keys.sort() l = [] for k in keys: v = ver[k] l.append(f'{k}@{v}') sep = ':' return sep.join(l) def version_footer(ver: dict, vintage='vyos') -> str: """ Version footer as string. """ ver_str = format_string(ver) release = get_version() if vintage == 'vyos': ret_str = (f'// Warning: Do not remove the following line.\n' + f'// vyos-config-version: "{ver_str}"\n' + f'// Release version: {release}\n') elif vintage == 'vyatta': ret_str = (f'/* Warning: Do not remove the following line. */\n' + f'/* === vyatta-config-version: "{ver_str}" === */\n' + f'/* Release version: {release} */\n') else: raise ValueError("Unknown config string vintage") return ret_str def system_footer(vintage='vyos') -> str: """ System version footer as string. """ ver_d = from_system() return version_footer(ver_d, vintage=vintage) def write_version_footer(ver: dict, file_name, vintage='vyos'): """ Write version footer to file. """ footer = version_footer(ver=ver, vintage=vintage) if file_name: with open(file_name, 'a') as f: f.write(footer) else: sys.stdout.write(footer) def write_system_footer(file_name, vintage='vyos'): """ Write system version footer to file. """ ver_d = from_system() return write_version_footer(ver_d, file_name=file_name, vintage=vintage) def remove_footer(file_name): """ Remove old version footer. """ for line in fileinput.input(file_name, inplace=True): if re.match(r'/\* Warning:.+ \*/$', line): continue if re.match(r'/\* === vyatta-config-version:.+=== \*/$', line): continue if re.match(r'/\* Release version:.+ \*/$', line): continue if re.match('// vyos-config-version:.+', line): continue if re.match('// Warning:.+', line): continue if re.match('// Release version:.+', line): continue sys.stdout.write(line)