summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/authutils.py43
-rw-r--r--python/vyos/defaults.py2
-rw-r--r--python/vyos/initialsetup.py72
-rw-r--r--python/vyos/keepalived.py153
-rw-r--r--python/vyos/util.py68
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)