summaryrefslogtreecommitdiff
path: root/python/vyos/utils
diff options
context:
space:
mode:
Diffstat (limited to 'python/vyos/utils')
-rw-r--r--python/vyos/utils/serial.py118
-rw-r--r--python/vyos/utils/vti_updown_db.py194
2 files changed, 312 insertions, 0 deletions
diff --git a/python/vyos/utils/serial.py b/python/vyos/utils/serial.py
new file mode 100644
index 000000000..b646f881e
--- /dev/null
+++ b/python/vyos/utils/serial.py
@@ -0,0 +1,118 @@
+# Copyright 2024 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 os, re, json
+from typing import List
+
+from vyos.base import Warning
+from vyos.utils.io import ask_yes_no
+from vyos.utils.process import cmd
+
+GLOB_GETTY_UNITS = 'serial-getty@*.service'
+RE_GETTY_DEVICES = re.compile(r'.+@(.+).service$')
+
+SD_UNIT_PATH = '/run/systemd/system'
+UTMP_PATH = '/run/utmp'
+
+def get_serial_units(include_devices=[]):
+ # Since we cannot depend on the current config for decommissioned ports,
+ # we just grab everything that systemd knows about.
+ tmp = cmd(f'systemctl list-units {GLOB_GETTY_UNITS} --all --output json --no-pager')
+ getty_units = json.loads(tmp)
+ for sdunit in getty_units:
+ m = RE_GETTY_DEVICES.search(sdunit['unit'])
+ if m is None:
+ Warning(f'Serial console unit name "{sdunit["unit"]}" is malformed and cannot be checked for activity!')
+ continue
+
+ getty_device = m.group(1)
+ if include_devices and getty_device not in include_devices:
+ continue
+
+ sdunit['device'] = getty_device
+
+ return getty_units
+
+def get_authenticated_ports(units):
+ connected = []
+ ports = [ x['device'] for x in units if 'device' in x ]
+ #
+ # utmpdump just gives us an easily parseable dump of currently logged-in sessions, for eg:
+ # $ utmpdump /run/utmp
+ # Utmp dump of /run/utmp
+ # [2] [00000] [~~ ] [reboot ] [~ ] [6.6.31-amd64-vyos ] [0.0.0.0 ] [2024-06-18T13:56:53,958484+00:00]
+ # [1] [00051] [~~ ] [runlevel] [~ ] [6.6.31-amd64-vyos ] [0.0.0.0 ] [2024-06-18T13:57:01,790808+00:00]
+ # [6] [03178] [tty1] [LOGIN ] [tty1 ] [ ] [0.0.0.0 ] [2024-06-18T13:57:31,015392+00:00]
+ # [7] [37151] [ts/0] [vyos ] [pts/0 ] [10.9.8.7 ] [10.9.8.7 ] [2024-07-04T13:42:08,760892+00:00]
+ # [8] [24812] [ts/1] [ ] [pts/1 ] [10.9.8.7 ] [10.9.8.7 ] [2024-06-20T18:10:07,309365+00:00]
+ #
+ # We can safely skip blank or LOGIN sessions with valid device names.
+ #
+ for line in cmd(f'utmpdump {UTMP_PATH}').splitlines():
+ row = line.split('] [')
+ user_name = row[3].strip()
+ user_term = row[4].strip()
+ if user_name and user_name != 'LOGIN' and user_term in ports:
+ connected.append(user_term)
+
+ return connected
+
+def restart_login_consoles(prompt_user=False, quiet=True, devices: List[str]=[]):
+ # restart_login_consoles() is called from both conf- and op-mode scripts, including
+ # the warning messages and user prompts common to both.
+ #
+ # The default case, called with no arguments, is a simple serial-getty restart &
+ # cleanup wrapper with no output or prompts that can be used from anywhere.
+ #
+ # quiet and prompt_user args have been split from an original "no_prompt", in
+ # order to support the completely silent default use case. "no_prompt" would
+ # only suppress the user interactive prompt.
+ #
+ # quiet intentionally does not suppress a vyos.base.Warning() for malformed
+ # device names in _get_serial_units().
+ #
+ cmd('systemctl daemon-reload')
+
+ units = get_serial_units(devices)
+ connected = get_authenticated_ports(units)
+
+ if connected:
+ if not quiet:
+ Warning('There are user sessions connected via serial console that '\
+ 'will be terminated when serial console settings are changed!')
+ if not prompt_user:
+ # This flag is used by conf_mode/system_console.py to reset things, if there's
+ # a problem, the user should issue a manual restart for serial-getty.
+ Warning('Please ensure all settings are committed and saved before issuing a ' \
+ '"restart serial console" command to apply new configuration!')
+ if not prompt_user:
+ return False
+ if not ask_yes_no('Any uncommitted changes from these sessions will be lost\n' \
+ 'and in-progress actions may be left in an inconsistent state.\n'\
+ '\nContinue?'):
+ return False
+
+ for unit in units:
+ if 'device' not in unit:
+ continue # malformed or filtered.
+ unit_name = unit['unit']
+ unit_device = unit['device']
+ if os.path.exists(os.path.join(SD_UNIT_PATH, unit_name)):
+ cmd(f'systemctl restart {unit_name}')
+ else:
+ # Deleted stubs don't need to be restarted, just shut them down.
+ cmd(f'systemctl stop {unit_name}')
+
+ return True
diff --git a/python/vyos/utils/vti_updown_db.py b/python/vyos/utils/vti_updown_db.py
new file mode 100644
index 000000000..b491fc6f2
--- /dev/null
+++ b/python/vyos/utils/vti_updown_db.py
@@ -0,0 +1,194 @@
+# Copyright 2024 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 os
+
+from contextlib import contextmanager
+from syslog import syslog
+
+VTI_WANT_UP_IFLIST = '/tmp/ipsec_vti_interfaces'
+
+def vti_updown_db_exists():
+ """ Returns true if the database exists """
+ return os.path.exists(VTI_WANT_UP_IFLIST)
+
+@contextmanager
+def open_vti_updown_db_for_create_or_update():
+ """ Opens the database for reading and writing, creating the database if it does not exist """
+ if vti_updown_db_exists():
+ f = open(VTI_WANT_UP_IFLIST, 'r+')
+ else:
+ f = open(VTI_WANT_UP_IFLIST, 'x+')
+ try:
+ db = VTIUpDownDB(f)
+ yield db
+ finally:
+ f.close()
+
+@contextmanager
+def open_vti_updown_db_for_update():
+ """ Opens the database for reading and writing, returning an error if it does not exist """
+ f = open(VTI_WANT_UP_IFLIST, 'r+')
+ try:
+ db = VTIUpDownDB(f)
+ yield db
+ finally:
+ f.close()
+
+@contextmanager
+def open_vti_updown_db_readonly():
+ """ Opens the database for reading, returning an error if it does not exist """
+ f = open(VTI_WANT_UP_IFLIST, 'r')
+ try:
+ db = VTIUpDownDB(f)
+ yield db
+ finally:
+ f.close()
+
+def remove_vti_updown_db():
+ """ Brings down any interfaces referenced by the database and removes the database """
+ # We need to process the DB first to bring down any interfaces still up
+ with open_vti_updown_db_for_update() as db:
+ db.removeAllOtherInterfaces([])
+ # this usage of commit will only ever bring down interfaces,
+ # do not need to provide a functional interface dict supplier
+ db.commit(lambda _: None)
+
+ os.unlink(VTI_WANT_UP_IFLIST)
+
+class VTIUpDownDB:
+ # The VTI Up-Down DB is a text-based database of space-separated "ifspecs".
+ #
+ # ifspecs can come in one of the two following formats:
+ #
+ # persistent format: <interface name>
+ # indicates the named interface should always be up.
+ #
+ # connection format: <interface name>:<connection name>:<protocol>
+ # indicates the named interface wants to be up due to an established
+ # connection <connection name> using the <protocol> protocol.
+ #
+ # The configuration tree and ipsec daemon connection up-down hook
+ # modify this file as needed and use it to determine when a
+ # particular event or configuration change should lead to changing
+ # the interface state.
+
+ def __init__(self, f):
+ self._fileHandle = f
+ self._ifspecs = set([entry.strip() for entry in f.read().split(" ") if entry and not entry.isspace()])
+ self._ifsUp = set()
+ self._ifsDown = set()
+
+ def add(self, interface, connection = None, protocol = None):
+ """
+ Adds a new entry to the DB.
+
+ If an interface name, connection name, and protocol are supplied,
+ creates a connection entry.
+
+ If only an interface name is specified, creates a persistent entry
+ for the given interface.
+ """
+ ifspec = f"{interface}:{connection}:{protocol}" if (connection is not None and protocol is not None) else interface
+ if ifspec not in self._ifspecs:
+ self._ifspecs.add(ifspec)
+ self._ifsUp.add(interface)
+ self._ifsDown.discard(interface)
+
+ def remove(self, interface, connection = None, protocol = None):
+ """
+ Removes a matching entry from the DB.
+
+ If no matching entry can be fonud, the operation returns successfully.
+ """
+ ifspec = f"{interface}:{connection}:{protocol}" if (connection is not None and protocol is not None) else interface
+ if ifspec in self._ifspecs:
+ self._ifspecs.remove(ifspec)
+ interface_remains = False
+ for ifspec in self._ifspecs:
+ if ifspec.split(':')[0] == interface:
+ interface_remains = True
+
+ if not interface_remains:
+ self._ifsDown.add(interface)
+ self._ifsUp.discard(interface)
+
+ def wantsInterfaceUp(self, interface):
+ """ Returns whether the DB contains at least one entry referencing the given interface """
+ for ifspec in self._ifspecs:
+ if ifspec.split(':')[0] == interface:
+ return True
+
+ return False
+
+ def removeAllOtherInterfaces(self, interface_list):
+ """ Removes all interfaces not included in the given list from the DB """
+ updated_ifspecs = set([ifspec for ifspec in self._ifspecs if ifspec.split(':')[0] in interface_list])
+ removed_ifspecs = self._ifspecs - updated_ifspecs
+ self._ifspecs = updated_ifspecs
+ interfaces_to_bring_down = [ifspec.split(':')[0] for ifspec in removed_ifspecs]
+ self._ifsDown.update(interfaces_to_bring_down)
+ self._ifsUp.difference_update(interfaces_to_bring_down)
+
+ def setPersistentInterfaces(self, interface_list):
+ """ Updates the set of persistently up interfaces to match the given list """
+ new_presistent_interfaces = set(interface_list)
+ current_presistent_interfaces = set([ifspec for ifspec in self._ifspecs if ':' not in ifspec])
+ added_presistent_interfaces = new_presistent_interfaces - current_presistent_interfaces
+ removed_presistent_interfaces = current_presistent_interfaces - new_presistent_interfaces
+
+ for interface in added_presistent_interfaces:
+ self.add(interface)
+
+ for interface in removed_presistent_interfaces:
+ self.remove(interface)
+
+ def commit(self, interface_dict_supplier):
+ """
+ Writes the DB to disk and brings interfaces up and down as needed.
+
+ Only interfaces referenced by entries modified in this DB session
+ are manipulated. If an interface is called to be brought up, the
+ provided interface_config_supplier function is invoked and expected
+ to return the config dictionary for the interface.
+ """
+ from vyos.ifconfig import VTIIf
+ from vyos.utils.process import call
+ from vyos.utils.network import get_interface_config
+
+ self._fileHandle.seek(0)
+ self._fileHandle.write(' '.join(self._ifspecs))
+ self._fileHandle.truncate()
+
+ for interface in self._ifsDown:
+ vti_link = get_interface_config(interface)
+ vti_link_up = (vti_link['operstate'] != 'DOWN' if 'operstate' in vti_link else False)
+ if vti_link_up:
+ call(f'sudo ip link set {interface} down')
+ syslog(f'Interface {interface} is admin down ...')
+
+ self._ifsDown.clear()
+
+ for interface in self._ifsUp:
+ vti_link = get_interface_config(interface)
+ vti_link_up = (vti_link['operstate'] != 'DOWN' if 'operstate' in vti_link else False)
+ if not vti_link_up:
+ vti = interface_dict_supplier(interface)
+ if 'disable' not in vti:
+ tmp = VTIIf(interface, bypass_vti_updown_db = True)
+ tmp.update(vti)
+ syslog(f'Interface {interface} is admin up ...')
+
+ self._ifsUp.clear()