diff options
-rw-r--r-- | data/templates/salt-minion/minion.tmpl | 10 | ||||
-rw-r--r-- | debian/control | 1 | ||||
-rw-r--r-- | debian/vyos-1x.postinst | 21 | ||||
-rw-r--r-- | interface-definitions/salt-minion.xml.in | 78 | ||||
-rw-r--r-- | python/vyos/ifconfig/dhcp.py | 4 | ||||
-rwxr-xr-x | src/conf_mode/salt-minion.py | 105 | ||||
-rwxr-xr-x | src/migration-scripts/salt/0-to-1 | 58 | ||||
-rwxr-xr-x | src/op_mode/powerctrl.py | 291 |
8 files changed, 314 insertions, 254 deletions
diff --git a/data/templates/salt-minion/minion.tmpl b/data/templates/salt-minion/minion.tmpl index 5e50d588c..9369573a4 100644 --- a/data/templates/salt-minion/minion.tmpl +++ b/data/templates/salt-minion/minion.tmpl @@ -12,7 +12,7 @@ # # Prior to changing this value, the master should be stopped and all Salt # caches should be cleared. -hash_type: {{ hash_type }} +hash_type: {{ hash }} ##### Logging settings ##### ########################################## @@ -21,11 +21,7 @@ hash_type: {{ hash_type }} # location. Remote logging works best when configured to use rsyslogd(8) (e.g.: # ``file:///dev/log``), with rsyslogd(8) configured for network logging. The URI # format is: <file|udp|tcp>://<host|socketpath>:<port-if-required>/<log-facility> -#log_file: /var/log/salt/minion -#log_file: file:///dev/log -#log_file: udp://loghost:10514 -# -log_file: {{ log_file }} +log_file: file:///dev/log # The level of messages to send to the console. # One of 'garbage', 'trace', 'debug', info', 'warning', 'error', 'critical'. @@ -58,6 +54,6 @@ id: {{ salt_id }} # The number of minutes between mine updates. -mine_interval: {{ mine_interval }} +mine_interval: {{ interval }} verify_master_pubkey_sign: {{ verify_master_pubkey_sign }} diff --git a/debian/control b/debian/control index 7b95b2c75..5c176f40a 100644 --- a/debian/control +++ b/debian/control @@ -89,6 +89,7 @@ Depends: python3, pmacct (>= 1.6.0), python3-certbot-nginx, pppoe, + salt-minion, ${shlibs:Depends}, ${misc:Depends} Description: VyOS configuration scripts and data diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst new file mode 100644 index 000000000..a308401ee --- /dev/null +++ b/debian/vyos-1x.postinst @@ -0,0 +1,21 @@ +#!/bin/sh -e +if ! deb-systemd-helper --quiet was-enabled salt-minion.service; then + # Enables the unit on first installation, creates new + # symlinks on upgrades if the unit file has changed. + deb-systemd-helper disable salt-minion.service >/dev/null || true +fi + +if [ -x "/etc/init.d/salt-minion" ]; then + update-rc.d -f salt-minion remove >/dev/null +fi + +# Add minion user for salt-minion +if ! grep -q '^minion' /etc/passwd; then + adduser --quiet --firstuid 100 --system --disabled-login --ingroup vyattacfg --gecos "salt minion user" --shell /bin/vbash minion + adduser --quiet minion frrvty + adduser --quiet minion sudo + adduser --quiet minion adm + adduser --quiet minion dip + adduser --quiet minion disk + adduser --quiet minion users +fi diff --git a/interface-definitions/salt-minion.xml.in b/interface-definitions/salt-minion.xml.in index 9aa60249a..97f882a6a 100644 --- a/interface-definitions/salt-minion.xml.in +++ b/interface-definitions/salt-minion.xml.in @@ -1,5 +1,4 @@ <?xml version="1.0"?> -<!--Salt-minion configuration --> <interfaceDefinition> <node name="service"> <children> @@ -9,73 +8,56 @@ <priority>500</priority> </properties> <children> - <leafNode name="hash_type"> + <leafNode name="hash"> <properties> - <help>The hash_type is the hash to use when discovering the hash of a file on the master server.</help> + <help>Hash used when discovering file on master server (default: sha256)</help> + <completionHelp> + <list>md5 sha1 sha224 sha256 sha384 sha512</list> + </completionHelp> + <constraint> + <regex>(md5|sha1|sha224|sha256|sha384|sha512)</regex> + </constraint> </properties> </leafNode> - <leafNode name="log_file"> - <properties> - <help>The location of the minion log file.</help> - </properties> - </leafNode> - <leafNode name="log_level"> + <leafNode name="master"> <properties> - <help>Log level</help> - <valueHelp> - <format>garbage</format> - <description>log garbage info</description> - </valueHelp> - <valueHelp> - <format>trace</format> - <description>log trace info</description> - </valueHelp> - <valueHelp> - <format>debug</format> - <description>log debug info</description> - </valueHelp> - <valueHelp> - <format>info</format> - <description>log info</description> - </valueHelp> - <valueHelp> - <format>warning</format> - <description>log warning info</description> - </valueHelp> + <help>The hostname or IP address of the master.</help> <valueHelp> - <format>error</format> - <description>log error info</description> + <format>ipv4</format> + <description>Remote syslog server IPv4 address</description> </valueHelp> <valueHelp> - <format>critical</format> - <description>log critical info</description> + <format>hostname</format> + <description>Remote syslog server FQDN</description> </valueHelp> - </properties> - </leafNode> - <leafNode name="master"> - <properties> - <help>The hostname or IP address of the master.</help> + <constraint> + <validator name="ip-address"/> + <validator name="fqdn"/> + </constraint> + <constraintErrorMessage>Invalid FQDN or IP address</constraintErrorMessage> <multi/> </properties> </leafNode> <leafNode name="id"> <properties> - <help>Explicitly declare the id for this minion to use.</help> - </properties> - </leafNode> - <leafNode name="user"> - <properties> - <help>The user to run the Salt processes.</help> + <help>Explicitly declare ID for this minion to use (default: hostname)</help> </properties> </leafNode> - <leafNode name="mine_interval"> + <leafNode name="interval"> <properties> - <help>The number of minutes between mine updates.</help> + <help>Interval in minutes between updates (default: 60)</help> + <valueHelp> + <format><1-1440></format> + <description>Update interval in minutes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-1440"/> + </constraint> </properties> </leafNode> <leafNode name="master-key"> <properties> - <help>Enables verification of the master-public-signature returned by the master in auth-replies.</help> + <help>URL with signature of master for auth reply verification</help> </properties> </leafNode> </children> diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py index d4ff9c2cd..3122147a3 100644 --- a/python/vyos/ifconfig/dhcp.py +++ b/python/vyos/ifconfig/dhcp.py @@ -187,14 +187,14 @@ class _DHCPv6 (_DHCP): # stop dhclient cmd = 'start-stop-daemon' - cmd += ' --start' + cmd += ' --stop' cmd += ' --oknodo' cmd += ' --quiet' cmd += ' --pidfile {pid}' self._cmd(cmd.format(**self.file)) # accept router announcements on this interface - self._write_sysfs(self.options['accept_ra'], 1) + self._write_sysfs(self.file['accept_ra'], 1) # cleanup old config files for name in ('conf', 'pid', 'lease'): diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py index 236480854..dffe7fcd4 100755 --- a/src/conf_mode/salt-minion.py +++ b/src/conf_mode/salt-minion.py @@ -17,117 +17,108 @@ import os from copy import deepcopy -from pwd import getpwnam from socket import gethostname from sys import exit from urllib3 import PoolManager from vyos.config import Config -from vyos import ConfigError -from vyos.util import call from vyos.template import render - +from vyos.util import call, chown +from vyos import ConfigError config_file = r'/etc/salt/minion' +master_keyfile = r'/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub' default_config_data = { - 'hash_type': 'sha256', - 'log_file': '/var/log/salt/minion', + 'hash': 'sha256', 'log_level': 'warning', 'master' : 'salt', 'user': 'minion', + 'group': 'vyattacfg', 'salt_id': gethostname(), 'mine_interval': '60', - 'verify_master_pubkey_sign': 'false' + 'verify_master_pubkey_sign': 'false', + 'master_key': '' } def get_config(): salt = deepcopy(default_config_data) conf = Config() - if not conf.exists('service salt-minion'): + base = ['service', 'salt-minion'] + + if not conf.exists(base): return None else: - conf.set_level('service salt-minion') - - if conf.exists('hash_type'): - salt['hash_type'] = conf.return_value('hash_type') + conf.set_level(base) - if conf.exists('log_file'): - salt['log_file'] = conf.return_value('log_file') + if conf.exists(['hash']): + salt['hash'] = conf.return_value(['hash']) - if conf.exists('log_level'): - salt['log_level'] = conf.return_value('log_level') + if conf.exists(['master']): + salt['master'] = conf.return_values(['master']) - if conf.exists('master'): - master = conf.return_values('master') - salt['master'] = master + if conf.exists(['id']): + salt['salt_id'] = conf.return_value(['id']) - if conf.exists('id'): - salt['salt_id'] = conf.return_value('id') + if conf.exists(['user']): + salt['user'] = conf.return_value(['user']) - if conf.exists('user'): - salt['user'] = conf.return_value('user') + if conf.exists(['interval']): + salt['interval'] = conf.return_value(['interval']) - if conf.exists('mine_interval'): - salt['mine_interval'] = conf.return_value('mine_interval') - - salt['master-key'] = None - if conf.exists('master-key'): - salt['master-key'] = conf.return_value('master-key') + if conf.exists(['master-key']): + salt['master_key'] = conf.return_value(['master-key']) salt['verify_master_pubkey_sign'] = 'true' return salt -def generate(salt): - paths = ['/etc/salt/','/var/run/salt','/opt/vyatta/etc/config/salt/'] - directory = '/opt/vyatta/etc/config/salt/pki/minion' - uid = getpwnam(salt['user']).pw_uid - http = PoolManager() +def verify(salt): + return None - if salt is None: +def generate(salt): + if not salt: return None - if not os.path.exists(directory): - os.makedirs(directory) + for file in [config_file, master_keyfile]: + dirname = os.path.dirname(file) + if not os.path.exists(dirname): + os.mkdir(dirname) + chown(dirname, salt['user'], salt['group']) render(config_file, 'salt-minion/minion.tmpl', salt) + chown(config_file, salt['user'], salt['group']) - path = "/etc/salt/" - for path in paths: - for root, dirs, files in os.walk(path): - for usgr in dirs: - os.chown(os.path.join(root, usgr), uid, 100) - for usgr in files: - os.chown(os.path.join(root, usgr), uid, 100) - - if not os.path.exists('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'): - if not salt['master-key'] is None: - r = http.request('GET', salt['master-key'], preload_content=False) + if not os.path.exists(master_keyfile): + if salt['master_key']: + req = PoolManager().request('GET', salt['master_key'], preload_content=False) - with open('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub', 'wb') as out: + with open(master_keyfile, 'wb') as f: while True: - data = r.read(1024) + data = req.read(1024) if not data: break - out.write(data) + f.write(data) - r.release_conn() + req.release_conn() + chown(master_keyfile, salt['user'], salt['group']) return None def apply(salt): - if salt is not None: - call("sudo systemctl restart salt-minion") + if not salt: + # Salt removed from running config + call('systemctl stop salt-minion.service') + if os.path.exists(config_file): + os.unlink(config_file) else: - # Salt access is removed in the commit - call("sudo systemctl stop salt-minion") - os.unlink(config_file) + call('systemctl restart salt-minion.service') return None if __name__ == '__main__': try: c = get_config() + verify(c) generate(c) apply(c) except ConfigError as e: diff --git a/src/migration-scripts/salt/0-to-1 b/src/migration-scripts/salt/0-to-1 new file mode 100755 index 000000000..79053c056 --- /dev/null +++ b/src/migration-scripts/salt/0-to-1 @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 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/>. + +# Delete log_file, log_level and user nodes +# rename hash_type to hash +# rename mine_interval to interval + +from sys import argv,exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['service', 'salt-minion'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + + # delete nodes which are now populated with sane defaults + for node in ['log_file', 'log_level', 'user']: + if config.exists(base + [node]): + config.delete(base + [node]) + + if config.exists(base + ['hash_type']): + config.rename(base + ['hash_type'], 'hash') + + if config.exists(base + ['mine_interval']): + config.rename(base + ['mine_interval'], 'interval') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index 4ab91384b..b2fb5fe5a 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -15,168 +15,179 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import sys -import argparse import re +from argparse import ArgumentParser from datetime import datetime, timedelta, time as type_time, date as type_date -from vyos.util import ask_yes_no -from vyos.util import cmd -from vyos.util import call -from vyos.util import run -from vyos.util import STDOUT +from sys import exit +from time import time + +from vyos.util import ask_yes_no, cmd, call, run, STDOUT systemd_sched_file = "/run/systemd/shutdown/scheduled" -def parse_time(s): - try: - if re.match(r'^\d{1,2}$', s): - return datetime.strptime(s, "%M").time() - else: - return datetime.strptime(s, "%H:%M").time() - except ValueError: - return None +def utc2local(datetime): + now = time() + offs = datetime.fromtimestamp(now) - datetime.utcfromtimestamp(now) + return datetime + offs -def parse_date(s): - for fmt in ["%d%m%Y", "%d/%m/%Y", "%d.%m.%Y", "%d:%m:%Y", "%Y-%m-%d"]: +def parse_time(s): try: - return datetime.strptime(s, fmt).date() + if re.match(r'^\d{1,2}$', s): + return datetime.strptime(s, "%M").time() + else: + return datetime.strptime(s, "%H:%M").time() except ValueError: - continue - # If nothing matched... - return None + return None + + +def parse_date(s): + for fmt in ["%d%m%Y", "%d/%m/%Y", "%d.%m.%Y", "%d:%m:%Y", "%Y-%m-%d"]: + try: + return datetime.strptime(s, fmt).date() + except ValueError: + continue + # If nothing matched... + return None + def get_shutdown_status(): - if os.path.exists(systemd_sched_file): - # Get scheduled from systemd file - with open(systemd_sched_file, 'r') as f: - data = f.read().rstrip('\n') - r_data = {} - for line in data.splitlines(): - tmp_split = line.split("=") - if tmp_split[0] == "USEC": - # Convert USEC to human readable format - r_data['DATETIME'] = datetime.utcfromtimestamp(int(tmp_split[1])/1000000).strftime('%Y-%m-%d %H:%M:%S') - else: - r_data[tmp_split[0]] = tmp_split[1] - return r_data - return None + if os.path.exists(systemd_sched_file): + # Get scheduled from systemd file + with open(systemd_sched_file, 'r') as f: + data = f.read().rstrip('\n') + r_data = {} + for line in data.splitlines(): + tmp_split = line.split("=") + if tmp_split[0] == "USEC": + # Convert USEC to human readable format + r_data['DATETIME'] = datetime.utcfromtimestamp( + int(tmp_split[1])/1000000).strftime('%Y-%m-%d %H:%M:%S') + else: + r_data[tmp_split[0]] = tmp_split[1] + return r_data + return None + def check_shutdown(): - output = get_shutdown_status() - if output and 'MODE' in output: - if output['MODE'] == 'reboot': - print("Reboot is scheduled", output['DATETIME']) - elif output['MODE'] == 'poweroff': - print("Poweroff is scheduled", output['DATETIME']) - else: - print("Reboot or poweroff is not scheduled") + output = get_shutdown_status() + dt = datetime.strptime(output['DATETIME'], '%Y-%m-%d %H:%M:%S') + if output and 'MODE' in output: + if output['MODE'] == 'reboot': + print("Reboot is scheduled", utc2local(dt)) + elif output['MODE'] == 'poweroff': + print("Poweroff is scheduled", utc2local(dt)) + else: + print("Reboot or poweroff is not scheduled") + def cancel_shutdown(): - output = get_shutdown_status() - if output and 'MODE' in output: - timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - try: - cmd('/sbin/shutdown -c --no-wall') - except OSError as e: - sys.exit("Could not cancel a reboot or poweroff: %s" % e) - message = "Scheduled %s has been cancelled %s" % (output['MODE'], timenow) - run(f'wall {message}') - else: - print("Reboot or poweroff is not scheduled") - -def execute_shutdown(time, reboot = True, ask=True): - if not ask: - action = "reboot" if reboot else "poweroff" - if not ask_yes_no("Are you sure you want to %s this system?" % action): - sys.exit(0) - - action = "-r" if reboot else "-P" - - if len(time) == 0: - ### T870 legacy reboot job support - chk_vyatta_based_reboots() - ### - - out = cmd(f'/sbin/shutdown {action} now', stderr=STDOUT) - print(out.split(",",1)[0]) - return - elif len(time) == 1: - # Assume the argument is just time - ts = parse_time(time[0]) - if ts: - cmd(f'/sbin/shutdown {action} {time[0]}', stderr=STDOUT) + output = get_shutdown_status() + if output and 'MODE' in output: + timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + try: + run('/sbin/shutdown -c --no-wall') + except OSError as e: + exit("Could not cancel a reboot or poweroff: %s" % e) + + message = 'Scheduled {} has been cancelled {}'.format(output['MODE'], timenow) + run(f'wall {message} > /dev/null 2>&1') else: - sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) - elif len(time) == 2: - # Assume it's date and time - ts = parse_time(time[0]) - ds = parse_date(time[1]) - if ts and ds: - t = datetime.combine(ds, ts) - td = t - datetime.now() - t2 = 1 + int(td.total_seconds())//60 # Get total minutes - cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT) + print("Reboot or poweroff is not scheduled") + + +def execute_shutdown(time, reboot=True, ask=True): + if not ask: + action = "reboot" if reboot else "poweroff" + if not ask_yes_no("Are you sure you want to %s this system?" % action): + exit(0) + + action = "-r" if reboot else "-P" + + if len(time) == 0: + # T870 legacy reboot job support + chk_vyatta_based_reboots() + ### + + out = cmd(f'/sbin/shutdown {action} now', stderr=STDOUT) + print(out.split(",", 1)[0]) + return + elif len(time) == 1: + # Assume the argument is just time + ts = parse_time(time[0]) + if ts: + cmd(f'/sbin/shutdown {action} {time[0]}', stderr=STDOUT) + else: + exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) + elif len(time) == 2: + # Assume it's date and time + ts = parse_time(time[0]) + ds = parse_date(time[1]) + if ts and ds: + t = datetime.combine(ds, ts) + td = t - datetime.now() + t2 = 1 + int(td.total_seconds())//60 # Get total minutes + cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT) + else: + if not ts: + exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) + else: + exit("Invalid time \"{0}\". A valid format is YYYY-MM-DD [HH:MM]".format(time[1])) else: - if not ts: - sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) - else: - sys.exit("Invalid time \"{0}\". A valid format is YYYY-MM-DD [HH:MM]".format(time[1])) - else: - sys.exit("Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM") - check_shutdown() + exit("Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM") + check_shutdown() + def chk_vyatta_based_reboots(): - ### T870 commit-confirm is still using the vyatta code base, once gone, the code below can be removed - ### legacy scheduled reboot s are using at and store the is as /var/run/<name>.job - ### name is the node of scheduled the job, commit-confirm checks for that + # T870 commit-confirm is still using the vyatta code base, once gone, the code below can be removed + # legacy scheduled reboot s are using at and store the is as /var/run/<name>.job + # name is the node of scheduled the job, commit-confirm checks for that + + f = r'/var/run/confirm.job' + if os.path.exists(f): + jid = open(f).read().strip() + if jid != 0: + call(f'sudo atrm {jid}') + os.remove(f) - f = r'/var/run/confirm.job' - if os.path.exists(f): - jid = open(f).read().strip() - if jid != 0: - call(f'sudo atrm {jid}') - os.remove(f) def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--yes", "-y", - help="Do not ask for confirmation", - action="store_true", - dest="yes") - action = parser.add_mutually_exclusive_group(required=True) - action.add_argument("--reboot", "-r", - help="Reboot the system", - nargs="*", - metavar="Minutes|HH:MM") - - action.add_argument("--poweroff", "-p", - help="Poweroff the system", - nargs="*", - metavar="Minutes|HH:MM") - - action.add_argument("--cancel", "-c", - help="Cancel pending shutdown", - action="store_true") - - action.add_argument("--check", - help="Check pending chutdown", - action="store_true") - args = parser.parse_args() - - try: - if args.reboot is not None: - execute_shutdown(args.reboot, reboot=True, ask=args.yes) - if args.poweroff is not None: - execute_shutdown(args.poweroff, reboot=False,ask=args.yes) - if args.cancel: - cancel_shutdown() - if args.check: - check_shutdown() - except KeyboardInterrupt: - sys.exit("Interrupted") + parser = ArgumentParser() + parser.add_argument("--yes", "-y", + help="Do not ask for confirmation", + action="store_true", + dest="yes") + action = parser.add_mutually_exclusive_group(required=True) + action.add_argument("--reboot", "-r", + help="Reboot the system", + nargs="*", + metavar="Minutes|HH:MM") + + action.add_argument("--poweroff", "-p", + help="Poweroff the system", + nargs="*", + metavar="Minutes|HH:MM") + + action.add_argument("--cancel", "-c", + help="Cancel pending shutdown", + action="store_true") + + action.add_argument("--check", + help="Check pending chutdown", + action="store_true") + args = parser.parse_args() + try: + if args.reboot is not None: + execute_shutdown(args.reboot, reboot=True, ask=args.yes) + if args.poweroff is not None: + execute_shutdown(args.poweroff, reboot=False, ask=args.yes) + if args.cancel: + cancel_shutdown() + if args.check: + check_shutdown() + except KeyboardInterrupt: + exit("Interrupted") if __name__ == "__main__": - main() - + main() |