summaryrefslogtreecommitdiff
path: root/ncserver/module/openrc.py
diff options
context:
space:
mode:
authorA. Wilcox <AWilcox@Wilcox-Tech.com>2020-09-28 19:59:11 -0500
committerA. Wilcox <AWilcox@Wilcox-Tech.com>2020-09-28 19:59:11 -0500
commit15d8e63671efc5e17dcac52630b7b400d9dd3829 (patch)
treecf4842722b601ee4466f3a670a3e9034b7731042 /ncserver/module/openrc.py
parente95e96647ab851306402f31b50d2bba30f173de9 (diff)
downloadnetconfapk-15d8e63671efc5e17dcac52630b7b400d9dd3829.tar.gz
netconfapk-15d8e63671efc5e17dcac52630b7b400d9dd3829.tar.bz2
netconfapk-15d8e63671efc5e17dcac52630b7b400d9dd3829.tar.xz
netconfapk-15d8e63671efc5e17dcac52630b7b400d9dd3829.zip
Add initial implementation of OpenRC manager module
Diffstat (limited to 'ncserver/module/openrc.py')
-rw-r--r--ncserver/module/openrc.py259
1 files changed, 259 insertions, 0 deletions
diff --git a/ncserver/module/openrc.py b/ncserver/module/openrc.py
new file mode 100644
index 0000000..60f6c7f
--- /dev/null
+++ b/ncserver/module/openrc.py
@@ -0,0 +1,259 @@
+"""
+NETCONF for APK Distributions server:
+ adelie-services module, OpenRC edition
+
+Copyright © 2020 Adélie Software in the Public Benefit, Inc.
+
+Released under the terms of the NCSA license. See the LICENSE file included
+with this source distribution for more information.
+
+SPDX-License-Identifier: NCSA
+"""
+
+import errno
+import logging
+import os
+import pathlib
+import subprocess
+
+from lxml import etree
+from netconf import error, util
+
+from ncserver.base.service import Service, ServiceStatus
+from ncserver.base.util import _
+
+
+QName = etree.QName # pylint: disable=I1101
+
+
+LOGGER = logging.getLogger(__name__)
+"""The object used for logging informational messages."""
+
+
+M_ABI_VERSION = 1
+"""The ABI version of this NETCONF module."""
+
+
+M_PREFIX = "svcs"
+"""The XML tag prefix for this module's tags."""
+
+
+M_NS = "http://netconf.adelielinux.org/ns/service"
+"""The XML namespace for this module."""
+
+
+M_NAME = "adelie-services"
+"""The YANG model name for this module."""
+
+
+M_REVISION = "2020-09-22"
+"""The YANG revision date for this module."""
+
+
+M_IMPORTS = {
+ 'ietf-yang-types@2013-07-15': {
+ 'ns': "urn:ietf:params:xml:ns:yang:ietf-yang-types", 'prefix': "yang"
+ }
+}
+"""The imported YANG modules for this module."""
+
+
+def valid_name(name):
+ """Determine if the given name is a valid OpenRC service name."""
+ valid = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_-."
+ return all(char in valid for char in name)
+
+
+def is_enabled(name):
+ """Determine if this OpenRC service is enabled."""
+ runlevels = (pathlib.Path('/etc/runlevels/boot'),
+ pathlib.Path('/etc/runlevels/sysinit'),
+ pathlib.Path('/etc/runlevels/default'))
+ for level in runlevels:
+ for service in level.iterdir():
+ # Resolve the symlink. If it's the service name, we have a match.
+ if pathlib.Path(service).resolve().name == name:
+ return True
+ return False
+
+
+def get_var(shell_like, var_name: str, default="") -> str:
+ """Retrieve the value of var_name from a shell script-like file shell_like.
+
+ :param shell_like:
+ The path to the shell script-like file.
+
+ :param str var_name:
+ The name of the variable to extract.
+
+ :param str default:
+ The default value to return if the variable is not set.
+ """
+ script = ". /etc/init.d/functions.sh; ( . {shlike} && printf %s \"${var}\" )"
+ script = script.format(shlike=shell_like, var=var_name)
+ proc = subprocess.run(['/bin/sh', '-e', '-c', script],
+ stdout=subprocess.PIPE, check=True, env={'SHDIR':'/etc/init.d'},
+ stderr=subprocess.PIPE
+ )
+ value = proc.stdout.decode('utf-8')
+ if value == "":
+ return default
+ return value
+
+
+def check_alive(pidfile) -> bool:
+ """Determine if the PID contained in a pidfile is alive."""
+ pid = 0
+ try:
+ with open(pidfile) as fhandle:
+ pid = int(fhandle.read().rstrip())
+ except OSError as ose:
+ if ose.errno == errno.EPERM or ose.errno == errno.EACCES:
+ return True # the pidfile cannot be accessed
+ return False # sounds bad to me
+ except ValueError:
+ return False # the pidfile doesn't contain a PID
+
+ if pid <= 0:
+ return False
+
+ try:
+ os.kill(pid, 0)
+ except OSError as ose:
+ if ose.errno == errno.EPERM:
+ return True # we can't send signal because it's alive
+ return False
+ else:
+ return True
+
+
+class OpenRCService(Service):
+ """The OpenRC implementation of the Service class."""
+ def __init__(self, name):
+ """Initialise the structure."""
+ super().__init__()
+
+ if not valid_name(name):
+ raise NameError(_("{}: invalid service name").format(name))
+
+ svcpath = "/etc/init.d/{name}".format(name=name)
+ if not os.path.exists(svcpath):
+ raise ValueError(_("{} does not exist").format(name))
+
+ self._name = name
+ self._description = get_var(svcpath, "description", name)
+ self._enabled = is_enabled(name)
+
+ def status(self):
+ """Retrieve the service's status."""
+ stat = ServiceStatus.Stopped
+
+ status_dirs = {
+ ServiceStatus.Starting: "/run/openrc/starting",
+ ServiceStatus.Running: "/run/openrc/started",
+ ServiceStatus.Stopping: "/run/openrc/stopping"
+ }
+
+ for value, directory in status_dirs.items():
+ for service in pathlib.Path(directory).iterdir():
+ if pathlib.Path(service).resolve().name == self.name:
+ stat = value
+ break
+
+ if stat == ServiceStatus.Running:
+ # Check if crashed
+ for service in pathlib.Path("/run/openrc/daemons").iterdir():
+ svcdir = pathlib.Path(service)
+ if svcdir.name == self.name:
+ for daemon in svcdir.iterdir():
+ dpid = get_var(daemon, 'pidfile')
+ if dpid and not check_alive(dpid):
+ return ServiceStatus.Crashed
+
+ return stat
+
+ def start(self):
+ """Start the service."""
+ if self.status() in (ServiceStatus.Starting, ServiceStatus.Running):
+ return
+
+ subprocess.Popen(["/sbin/rc-service", self.name, "start"])
+
+ def stop(self):
+ """Stop the service."""
+ subprocess.Popen(["/sbin/rc-service", self.name, "stop"])
+
+ def reload(self):
+ """Reload the service's configuration, if possible."""
+
+ def restart(self, full: bool):
+ """Restart the service.
+
+ OpenRC does not support a concept of full restart, so this parameter
+ is always ignored.
+ """
+ subprocess.Popen(["/sbin/rc-service", self.name, "restart"])
+
+ def __str__(self):
+ return self.name
+
+ def __repr__(self):
+ return "<OpenRC service {name} ({status})>".format(
+ name=self.name, status=self.status()
+ )
+
+
+def service_list():
+ """Return the list of services available on this device."""
+ services = list()
+
+ for service in pathlib.Path('/etc/init.d').iterdir():
+ if service.suffix == '.sh':
+ continue
+
+ services.append(OpenRCService(service.name))
+
+ return services
+
+
+def info(service: str) -> OpenRCService:
+ """Return the Service object associated with the specified service.
+
+ :param str service:
+ The name of the service to look up.
+
+ :returns:
+ The Service object, or None if a service with that name does not exist.
+ """
+ try:
+ return OpenRCService(service)
+ except ValueError:
+ return None
+
+
+def running(node):
+ """Retrieve the service configuration for this device."""
+ svcs = util.subelm(node, 'svcs:services')
+
+ for service in service_list():
+ svcnode = util.subelm(svcs, 'svcs:service')
+ svcnode.append(util.leaf_elm('svcs:name', service.name))
+ svcnode.append(util.leaf_elm('svcs:enabled', service.enabled))
+
+
+def operational(node):
+ """Retrieve the service state for this device."""
+ svcs = util.subelm(node, 'svcs:services')
+
+ for service in service_list():
+ svcnode = util.subelm(svcs, 'svcs:service')
+ svcnode.append(util.leaf_elm('svcs:name', service.name))
+ svcnode.append(util.leaf_elm('svcs:description', service.description))
+ svcnode.append(util.leaf_elm('svcs:enabled', service.enabled))
+ svcnode.append(util.leaf_elm(
+ 'svcs:status', service.status().name.lower()
+ ))
+
+
+def edit(rpc, node, def_op):
+ """Edit the service configuration for this device."""