diff options
| -rw-r--r-- | data/op-mode-standardized.json | 1 | ||||
| -rw-r--r-- | interface-definitions/include/url.xml.i | 15 | ||||
| -rw-r--r-- | interface-definitions/system-update-check.xml.in | 22 | ||||
| -rw-r--r-- | op-mode-definitions/show-system.xml.in | 6 | ||||
| -rw-r--r-- | python/vyos/version.py | 39 | ||||
| -rwxr-xr-x | src/conf_mode/system_update_check.py | 93 | ||||
| -rwxr-xr-x | src/op_mode/system.py | 92 | ||||
| -rwxr-xr-x | src/system/vyos-system-update-check.py | 70 | ||||
| -rw-r--r-- | src/systemd/vyos-system-update.service | 11 | 
9 files changed, 349 insertions, 0 deletions
diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index 2d6f6da41..9500d3aa7 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -8,6 +8,7 @@  "neighbor.py",  "openconnect.py",  "route.py", +"system.py",  "ipsec.py",  "storage.py",  "uptime.py", diff --git a/interface-definitions/include/url.xml.i b/interface-definitions/include/url.xml.i new file mode 100644 index 000000000..caa6f67bd --- /dev/null +++ b/interface-definitions/include/url.xml.i @@ -0,0 +1,15 @@ +<!-- include start from url.xml.i --> +<leafNode name="url"> +  <properties> +    <help>Remote URL</help> +    <valueHelp> +      <format>url</format> +      <description>Remote URL</description> +    </valueHelp> +    <constraint> +      <regex>^https?:\/\/?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*(\:[0-9]+)*(\/.*)?</regex> +    </constraint> +    <constraintErrorMessage>Incorrect URL format</constraintErrorMessage> +  </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/system-update-check.xml.in b/interface-definitions/system-update-check.xml.in new file mode 100644 index 000000000..e4d7041ec --- /dev/null +++ b/interface-definitions/system-update-check.xml.in @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> +  <node name="system"> +    <children> +      <node name="update-check" owner="${vyos_conf_scripts_dir}/system_update_check.py"> +        <properties> +          <help>Check available update images</help> +          <priority>9999</priority> +        </properties> +        <children> +          <leafNode name="auto-check"> +            <properties> +              <help>Enable auto check for new images</help> +              <valueless/> +            </properties> +          </leafNode> +          #include <include/url.xml.i> +        </children> +      </node> +    </children> +  </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-system.xml.in b/op-mode-definitions/show-system.xml.in index bd32992aa..4a0e6c3b2 100644 --- a/op-mode-definitions/show-system.xml.in +++ b/op-mode-definitions/show-system.xml.in @@ -164,6 +164,12 @@              </properties>              <command>${vyos_op_scripts_dir}/storage.py show</command>            </leafNode> +          <leafNode name="updates"> +            <properties> +              <help>Show system available updates</help> +            </properties> +            <command>${vyos_op_scripts_dir}/system.py show_update</command> +          </leafNode>            <leafNode name="uptime">              <properties>                <help>Show system uptime and load averages</help> diff --git a/python/vyos/version.py b/python/vyos/version.py index 871bb0f1b..fb706ad44 100644 --- a/python/vyos/version.py +++ b/python/vyos/version.py @@ -31,6 +31,7 @@ Example of the version data dict::  import os  import json +import requests  import vyos.defaults  from vyos.util import read_file @@ -105,3 +106,41 @@ def get_full_version_data(fname=version_file):      version_data['hardware_uuid'] = read_file(subsystem + '/product_uuid', 'Unknown')      return version_data + +def get_remote_version(url): +    """ +    Get remote available JSON file from remote URL +    An example of the image-version.json + +    [ +       { +          "arch":"amd64", +          "flavors":[ +           "generic" +        ], +        "image":"vyos-rolling-latest.iso", +        "latest":true, +        "lts":false, +        "release_date":"2022-09-06", +        "release_train":"sagitta", +        "url":"http://xxx/rolling/current/vyos-rolling-latest.iso", +        "version":"vyos-1.4-rolling-202209060217" +      } +    ] +    """ +    headers = {} +    try: +        remote_data = requests.get(url=url, headers=headers) +        remote_data.raise_for_status() +        if remote_data.status_code != 200: +            return False +        return remote_data.json() +    except requests.exceptions.HTTPError as errh: +        print ("HTTP Error:", errh) +    except requests.exceptions.ConnectionError as errc: +        print ("Connecting error:", errc) +    except requests.exceptions.Timeout as errt: +        print ("Timeout error:", errt) +    except requests.exceptions.RequestException as err: +        print ("Unable to get remote data", err) +    return False diff --git a/src/conf_mode/system_update_check.py b/src/conf_mode/system_update_check.py new file mode 100755 index 000000000..08ecfcb81 --- /dev/null +++ b/src/conf_mode/system_update_check.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 +# 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 <http://www.gnu.org/licenses/>. + +import os +import json +import jmespath + +from pathlib import Path +from sys import exit + +from vyos.config import Config +from vyos.util import call +from vyos import ConfigError +from vyos import airbag +airbag.enable() + + +base = ['system', 'update-check'] +service_name = 'vyos-system-update' +service_conf = Path(f'/run/{service_name}.conf') +motd_file = Path('/run/motd.d/10-vyos-update') + + +def get_config(config=None): +    if config: +        conf = config +    else: +        conf = Config() + +    if not conf.exists(base): +        return None + +    config = conf.get_config_dict(base, key_mangling=('-', '_'), +                                  get_first_key=True, no_tag_node_value_mangle=True) + +    return config + + +def verify(config): +    # bail out early - looks like removal from running config +    if config is None: +        return + +    if 'url' not in config: +        raise ConfigError('URL is required!') + + +def generate(config): +    # bail out early - looks like removal from running config +    if config is None: +        # Remove old config and return +        service_conf.unlink(missing_ok=True) +        # MOTD used in /run/motd.d/10-update +        motd_file.unlink(missing_ok=True) +        return None + +    # Write configuration file +    conf_json = json.dumps(config, indent=4) +    service_conf.write_text(conf_json) + +    return None + + +def apply(config): +    if config: +        if 'auto_check' in config: +            call(f'systemctl restart {service_name}.service') +    else: +        call(f'systemctl stop {service_name}.service') + + +if __name__ == '__main__': +    try: +        c = get_config() +        verify(c) +        generate(c) +        apply(c) +    except ConfigError as e: +        print(e) +        exit(1) diff --git a/src/op_mode/system.py b/src/op_mode/system.py new file mode 100755 index 000000000..11a3a8730 --- /dev/null +++ b/src/op_mode/system.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 +# 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 <http://www.gnu.org/licenses/>. + +import jmespath +import json +import sys +import requests +import typing + +from sys import exit + +from vyos.configquery import ConfigTreeQuery + +import vyos.opmode +import vyos.version + +config = ConfigTreeQuery() +base = ['system', 'update-check'] + + +def _compare_version_raw(): +    url = config.value(base + ['url']) +    local_data = vyos.version.get_full_version_data() +    remote_data = vyos.version.get_remote_version(url) +    if not remote_data: +        return {"error": True, +                "reason": "Unable to get remote version"} +    if local_data.get('version') and remote_data: +        local_version = local_data.get('version') +        remote_version = jmespath.search('[0].version', remote_data) +        image_url = jmespath.search('[0].url', remote_data) +        if local_data.get('version') != remote_version: +            return {"error": False, +                    "update_available": True, +                    "local_version": local_version, +                    "remote_version": remote_version, +                    "url": image_url} +        return {"update_available": False, +                "local_version": local_version, +                "remote_version": remote_version} + + +def _formatted_compare_version(data): +    local_version = data.get('local_version') +    remote_version = data.get('remote_version') +    url = data.get('url') +    if {'update_available','local_version', 'remote_version', 'url'} <= set(data): +        return f'Current version: {local_version}\n\nUpdate available: {remote_version}\nUpdate URL: {url}' +    elif local_version == remote_version and remote_version is not None: +        return f'No available updates for your system \n' \ +               f'current version: {local_version}\nremote version: {remote_version}' +    else: +        return 'Update not found' + + +def _verify(): +    if not config.exists(base): +        return False +    return True + + +def show_update(raw: bool): +    if not _verify(): +        raise vyos.opmode.UnconfiguredSubsystem("system update-check not configured") +    data = _compare_version_raw() +    if raw: +        return data +    else: +        return _formatted_compare_version(data) + + +if __name__ == '__main__': +    try: +        res = vyos.opmode.run(sys.modules[__name__]) +        if res: +            print(res) +    except (ValueError, vyos.opmode.Error) as e: +        print(e) +        sys.exit(1) diff --git a/src/system/vyos-system-update-check.py b/src/system/vyos-system-update-check.py new file mode 100755 index 000000000..c9597721b --- /dev/null +++ b/src/system/vyos-system-update-check.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 +# 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 <http://www.gnu.org/licenses/>. + +import argparse +import json +import jmespath + +from pathlib import Path +from sys import exit +from time import sleep + +from vyos.util import call + +import vyos.version + +motd_file = Path('/run/motd.d/10-vyos-update') + + +if __name__ == '__main__': +    # Parse command arguments and get config +    parser = argparse.ArgumentParser() +    parser.add_argument('-c', +                        '--config', +                        action='store', +                        help='Path to system-update-check configuration', +                        required=True, +                        type=Path) + +    args = parser.parse_args() +    try: +        config_path = Path(args.config) +        config = json.loads(config_path.read_text()) +    except Exception as err: +        print( +            f'Configuration file "{config_path}" does not exist or malformed: {err}' +        ) +        exit(1) + +    url_json = config.get('url') +    local_data = vyos.version.get_full_version_data() +    local_version = local_data.get('version') + +    while True: +        remote_data = vyos.version.get_remote_version(url_json) +        if remote_data: +            url = jmespath.search('[0].url', remote_data) +            remote_version = jmespath.search('[0].version', remote_data) +            if local_version != remote_version and remote_version: +                call(f'wall -n "Update available: {remote_version} \nUpdate URL: {url}"') +                # MOTD used in /run/motd.d/10-update +                motd_file.parent.mkdir(exist_ok=True) +                motd_file.write_text(f'---\n' +                                     f'Current version: {local_version}\n' +                                     f'Update available: \033[1;34m{remote_version}\033[0m\n' +                                     f'---\n') +        # Check every 12 hours +        sleep(43200) diff --git a/src/systemd/vyos-system-update.service b/src/systemd/vyos-system-update.service new file mode 100644 index 000000000..032e5a14c --- /dev/null +++ b/src/systemd/vyos-system-update.service @@ -0,0 +1,11 @@ +[Unit] +Description=VyOS system udpate-check service +After=network.target vyos-router.service + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/python3 /usr/libexec/vyos/system/vyos-system-update-check.py --config /run/vyos-system-update.conf + +[Install] +WantedBy=multi-user.target  | 
