diff options
Diffstat (limited to 'python')
-rw-r--r-- | python/vyos/authutils.py | 43 | ||||
-rw-r--r-- | python/vyos/defaults.py | 2 | ||||
-rw-r--r-- | python/vyos/initialsetup.py | 72 | ||||
-rw-r--r-- | python/vyos/keepalived.py | 153 | ||||
-rw-r--r-- | python/vyos/util.py | 68 |
5 files changed, 338 insertions, 0 deletions
diff --git a/python/vyos/authutils.py b/python/vyos/authutils.py new file mode 100644 index 000000000..234294649 --- /dev/null +++ b/python/vyos/authutils.py @@ -0,0 +1,43 @@ +# authutils -- miscelanneous functions for handling passwords and publis keys +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import re + +from subprocess import Popen, PIPE, STDOUT + + +def make_password_hash(password): + """ Makes a password hash for /etc/shadow using mkpasswd """ + + mkpasswd = Popen(['mkpasswd', '--method=sha-512', '--stdin'], stdout=PIPE, stdin=PIPE, stderr=PIPE) + hash = mkpasswd.communicate(input=password.encode(), timeout=5)[0].decode().strip() + + return hash + +def split_ssh_public_key(key_string, defaultname=""): + """ Splits an SSH public key into its components """ + + key_string = key_string.strip() + parts = re.split(r'\s+', key_string) + + if len(parts) == 3: + key_type, key_data, key_name = parts[0], parts[1], parts[2] + else: + key_type, key_data, key_name = parts[0], parts[1], defaultname + + if key_type not in ['ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519']: + raise ValueError("Bad key type \'{0}\', must be one of must be one of ssh-rsa, ssh-dss, ecdsa-sha2-nistp<256|384|521> or ssh-ed25519".format(key_type)) + + return({"type": key_type, "data": key_data, "name": key_name}) diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index ac831c176..36185f16a 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -17,3 +17,5 @@ directories = { "data": "/usr/share/vyos/" } + +cfg_group = 'vyattacfg' diff --git a/python/vyos/initialsetup.py b/python/vyos/initialsetup.py new file mode 100644 index 000000000..574e7892d --- /dev/null +++ b/python/vyos/initialsetup.py @@ -0,0 +1,72 @@ +# initialsetup -- functions for setting common values in config file, +# for use in installation and first boot scripts +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import vyos.configtree +import vyos.authutils + +def set_interface_address(config, intf, addr, intf_type="ethernet"): + config.set(["interfaces", intf_type, intf, "address"], value=addr) + config.set_tag(["interfaces", intf_type]) + +def set_host_name(config, hostname): + config.set(["system", "host-name"], value=hostname) + +def set_name_servers(config, servers): + for s in servers: + config.set(["system", "name-server"], replace=False, value=s) + +def set_default_gateway(config, gateway): + config.set(["protocols", "static", "route", "0.0.0.0/0", "next-hop", gateway]) + config.set_tag(["protocols", "static", "route"]) + config.set_tag(["protocols", "static", "route", "0.0.0.0/0", "next-hop"]) + +def set_user_password(config, user, password): + # Make a password hash + hash = vyos.authutils.make_password_hash(password) + + config.set(["system", "login", "user", user, "authentication", "encrypted-password"], value=hash) + config.set(["system", "login", "user", user, "authentication", "plaintext-password"], value="") + +def disable_user_password(config, user): + config.set(["system", "login", "user", user, "authentication", "encrypted-password"], value="!") + config.set(["system", "login", "user", user, "authentication", "plaintext-password"], value="") + +def set_user_level(config, user, level): + config.set(["system", "login", "user", user, "level"], value=level) + +def set_user_ssh_key(config, user, key_string): + key = vyos.authutils.split_ssh_public_key(key_string, defaultname=user) + + config.set(["system", "login", "user", user, "authentication", "public-keys", key["name"], "key"], value=key["data"]) + config.set(["system", "login", "user", user, "authentication", "public-keys", key["name"], "type"], value=key["type"]) + config.set_tag(["system", "login", "user", user, "authentication", "public-keys"]) + +def create_user(config, user, password=None, key=None, level="admin"): + config.set(["system", "login", "user", user]) + config.set_tag(["system", "login", "user", user]) + + if not key and not password: + raise ValueError("Must set at least password or SSH public key") + + if password: + set_user_password(config, user, password) + else: + disable_user_password(config, user) + + if key: + set_user_ssh_key(config, user, key) + + set_user_level(config, user, level) diff --git a/python/vyos/keepalived.py b/python/vyos/keepalived.py new file mode 100644 index 000000000..4114aa736 --- /dev/null +++ b/python/vyos/keepalived.py @@ -0,0 +1,153 @@ +# Copyright 2018 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/>. + +import re +import os +import signal +import json + +import vyos.util + + +pid_file = '/var/run/keepalived.pid' +state_file = '/tmp/keepalived.data' +stats_file = '/tmp/keepalived.stats' +json_file = '/tmp/keepalived.json' + +state_dir = '/var/run/vyos/vrrp/' + +def vrrp_running(): + if not os.path.exists(vyos.keepalived.pid_file) \ + or not vyos.util.process_running(vyos.keepalived.pid_file): + return False + else: + return True + +def keepalived_running(): + return vyos.util.process_running(pid_file) + +def force_state_data_dump(): + pid = vyos.util.read_file(pid_file) + os.kill(int(pid), signal.SIGUSR1) + +def force_stats_dump(): + pid = vyos.util.read_file(pid_file) + os.kill(int(pid), signal.SIGUSR2) + +def force_json_dump(): + pid = vyos.util.read_file(pid_file) + os.kill(int(pid), signal.SIGRTMIN+2) + +def get_json_data(): + with open(json_file, 'r') as f: + j = json.load(f) + return j + +def get_statistics(): + return vyos.util.read_file(stats_file) + +def get_state_data(): + return vyos.util.read_file(state_file) + +def decode_state(code): + state = None + if code == 0: + state = "INIT" + elif code == 1: + state = "BACKUP" + elif code == 2: + state = "MASTER" + elif code == 3: + state = "FAULT" + else: + state = "UNKNOWN" + + return state + +## The functions are mainly for transition script wrappers +## to compensate for the fact that keepalived doesn't keep persistent +## state between reloads. +def get_old_state(group): + file = os.path.join(state_dir, "{0}.state".format(group)) + if os.path.exists(file): + with open(file, 'r') as f: + data = f.read().strip() + return data + else: + return None + +def save_state(group, state): + if not os.path.exists(state_dir): + os.makedirs(state_dir) + + file = os.path.join(state_dir, "{0}.state".format(group)) + with open(file, 'w') as f: + f.write(state) + +## These functions are for the old, and hopefully obsolete plaintext +## (non machine-readable) data format introduced by Vyatta back in the days +## They are kept here just in case, if JSON output option turns out or becomes +## insufficient. + +def read_state_data(): + with open(state_file, 'r') as f: + lines = f.readlines() + return lines + +def parse_keepalived_data(data_lines): + vrrp_groups = {} + + # Scratch variable + group_name = None + + # Sadly there is no explicit end marker in that format, so we have + # only two states, one before the first VRRP instance is encountered + # and one after an instance/"group" was encountered + # We'll set group_name once the first group is encountered, + # and assume we are inside a group if it's set afterwards + # + # It may not be necessary since the keywords found inside groups and before + # the VRRP Topology section seem to have no intersection, + # but better safe than sorry. + + for line in data_lines: + if re.match(r'^\s*VRRP Instance', line, re.IGNORECASE): + # Example: "VRRP Instance = Foo" + name = re.match(r'^\s*VRRP Instance\s+=\s+(.*)$', line, re.IGNORECASE).groups()[0].strip() + group_name = name + vrrp_groups[name] = {} + elif re.match(r'^\s*State', line, re.IGNORECASE) and group_name: + # Example: " State = MASTER" + group_state = re.match(r'^\s*State\s+=\s+(.*)$', line, re.IGNORECASE).groups()[0].strip() + vrrp_groups[group_name]["state"] = group_state + elif re.match(r'^\s*Last transition', line, re.IGNORECASE) and group_name: + # Example: " Last transition = 1532043820 (Thu Jul 19 23:43:40 2018)" + trans_time = re.match(r'^\s*Last transition\s+=\s+(\d+)\s', line, re.IGNORECASE).groups()[0] + vrrp_groups[group_name]["last_transition"] = trans_time + elif re.match(r'^\s*Interface', line, re.IGNORECASE) and group_name: + # Example: " Interface = eth0.30" + interface = re.match(r'\s*Interface\s+=\s+(.*)$', line, re.IGNORECASE).groups()[0].strip() + vrrp_groups[group_name]["interface"] = interface + elif re.match(r'^\s*Virtual Router ID', line, re.IGNORECASE) and group_name: + # Example: " Virtual Router ID = 14" + vrid = re.match(r'^\s*Virtual Router ID\s+=\s+(.*)$', line, re.IGNORECASE).groups()[0].strip() + vrrp_groups[group_name]["vrid"] = vrid + elif re.match(r'^\s*------< Interfaces', line, re.IGNORECASE): + # Interfaces section appears to always be present, + # and there's nothing of interest for us below that section, + # so we use it as an end of input marker + break + + return vrrp_groups diff --git a/python/vyos/util.py b/python/vyos/util.py index 8b3de7999..8b5342575 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -13,8 +13,19 @@ # 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/>. +import os import re +import grp +import psutil +import vyos.defaults + + +def read_file(path): + """ Read a file to string """ + with open(path, 'r') as f: + data = f.read().strip() + return data def colon_separated_to_dict(data_string, uniquekeys=False): """ Converts a string containing newline-separated entries @@ -63,3 +74,60 @@ def colon_separated_to_dict(data_string, uniquekeys=False): pass return data + +def process_running(pid_file): + """ Checks if a process with PID in pid_file is running """ + with open(pid_file, 'r') as f: + pid = f.read().strip() + return psutil.pid_exists(int(pid)) + +def seconds_to_human(s, separator=""): + """ Converts number of seconds passed to a human-readable + interval such as 1w4d18h35m59s + """ + s = int(s) + + week = 60 * 60 * 24 * 7 + day = 60 * 60 * 24 + hour = 60 * 60 + + remainder = 0 + result = "" + + weeks = s // week + if weeks > 0: + result = "{0}w".format(weeks) + s = s % week + + days = s // day + if days > 0: + result = "{0}{1}{2}d".format(result, separator, days) + s = s % day + + hours = s // hour + if hours > 0: + result = "{0}{1}{2}h".format(result, separator, hours) + s = s % hour + + minutes = s // 60 + if minutes > 0: + result = "{0}{1}{2}m".format(result, separator, minutes) + s = s % 60 + + seconds = s + if seconds > 0: + result = "{0}{1}{2}s".format(result, separator, seconds) + + return result + +def get_cfg_group_id(): + group_data = grp.getgrnam(vyos.defaults.cfg_group) + return group_data.gr_gid + +def file_is_persistent(path): + if not re.match(r'^(/config|/opt/vyatta/etc/config)', os.path.dirname(path)): + warning = "Warning: file {0} is outside the /config directory\n".format(path) + warning += "It will not be automatically migrated to a new image on system update" + return (False, warning) + else: + return (True, None) |