├── Dockerfile ├── README.md ├── openmanage.sh └── resources ├── entrypoint.sh ├── snmpd.conf └── systemctl.py /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use CentOS 7 base image from Docker Hub 2 | FROM centos:7.6.1810 3 | MAINTAINER Steve Kamerman "https://github.com/kamermans" 4 | #MAINTAINER Jose De la Rosa "https://github.com/jose-delarosa" 5 | 6 | # Environment variables 7 | ENV PATH $PATH:/opt/dell/srvadmin/bin:/opt/dell/srvadmin/sbin 8 | ENV TOMCATCFG /opt/dell/srvadmin/lib64/openmanage/apache-tomcat/conf/server.xml 9 | ENV TERM xterm 10 | ENV USER root 11 | ENV PASS password 12 | 13 | # Prevent daemon helper scripts from making systemd calls 14 | ENV SYSTEMCTL_SKIP_REDIRECT=1 15 | ENV container docker 16 | 17 | # Do overall update and install missing packages needed for OpenManage 18 | RUN mkdir -p /run/lock/subsys \ 19 | && echo "$USER:$PASS" | chpasswd \ 20 | # Add OMSA repo 21 | && yum -y install \ 22 | gcc wget perl passwd which tar \ 23 | nano dmidecode strace less openssl-devel \ 24 | # Strip systemd so it can run inside Docker 25 | # Note: "srvadmin-services.sh enable" doesn't work here because systemd is not PID 1 at build-time (it will be when it's run) 26 | && (cd /usr/lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == \ 27 | systemd-tmpfiles-setup.service ] || rm -f $i; done); \ 28 | rm -f /usr/lib/systemd/system/multi-user.target.wants/*; \ 29 | rm -f /etc/systemd/system/*.wants/*; \ 30 | rm -f /usr/lib/systemd/system/local-fs.target.wants/*; \ 31 | rm -f /usr/lib/systemd/system/sockets.target.wants/*udev*; \ 32 | rm -f /usr/lib/systemd/system/sockets.target.wants/*initctl*; \ 33 | rm -f /usr/lib/systemd/system/basic.target.wants/*; \ 34 | rm -f /usr/lib/systemd/system/anaconda.target.wants/*; \ 35 | wget -q -O - http://linux.dell.com/repo/hardware/dsu/bootstrap.cgi | bash \ 36 | && yum -y install \ 37 | net-snmp \ 38 | srvadmin-all \ 39 | ipmitool \ 40 | dell-system-update \ 41 | && yum clean all \ 42 | && localedef -i en_US -f UTF-8 en_US.UTF-8 \ 43 | && for SVC in snmpd instsvcdrv dsm_sa_eventmgrd dsm_sa_datamgrd dsm_sa_snmpd dsm_om_connsvc; do systemctl enable $SVC.service; done \ 44 | # Replace weak Diffie-Hellman ciphers with Elliptic-Curve Diffie-Hellman 45 | # Symlink in older libstorlibir for sasdupie segfault 46 | && sed -i \ 47 | -e 's/SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA/TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256/' \ 48 | -e 's/TLS_DHE_RSA_WITH_AES_128_CBC_SHA/TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA/' \ 49 | -e 's/TLS_DHE_DSS_WITH_AES_128_CBC_SHA/TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384/' \ 50 | -e 's/SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA/TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA/' $TOMCATCFG \ 51 | && ln -sf /opt/dell/srvadmin/lib64/libstorelibir-3.so /opt/dell/srvadmin/lib64/libstorelibir.so.5 \ 52 | && echo "dmidecode -t1" >> ~/.bashrc 53 | 54 | # Replace systemctl with a partial reimplementation for docker images 55 | # @see: https://github.com/gdraheim/docker-systemctl-replacement 56 | COPY ./resources/systemctl.py /usr/bin/systemctl 57 | 58 | # Note: the entrypoint script must contain systemd in the first 59 | # 16 characters of its name so that the Dell srvadmin-services.sh script 60 | # thinks its running with systemd as PID 1 and executes systemd services 61 | COPY ./resources/entrypoint.sh /fake-systemd-entrypoint.sh 62 | 63 | COPY resources/snmpd.conf /etc/snmp/snmpd.conf 64 | 65 | ENTRYPOINT ["/fake-systemd-entrypoint.sh"] 66 | CMD ["tail", "-f", "/opt/dell/srvadmin/var/log/openmanage/*.xml"] 67 | 68 | WORKDIR /opt/dell/srvadmin/bin 69 | 70 | EXPOSE 1311 161 162 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-openmanage 2 | 3 | Dell OpenManage and Dell System Update (`dsu`) running in a self-contained Docker container. 4 | 5 | > Note: this project includes the [`systemctl` replacement from @gdraheim](https://github.com/gdraheim/docker-systemctl-replacement). 6 | 7 | # Compatibility 8 | The host operating system must be Linux, and is known to work in RHEL, CentOS, Debian and Ubuntu. The release version of the distribution is not particularly important since they all share the Linux kernel, although it is possible that some issues could exist. 9 | 10 | This image has been tested on the following Dell hardware: 11 | 12 | | Hardware | Status | 13 | |---------------------|--------------| 14 | | Dell PowerEdge R610 | OK | 15 | | Dell PowerEdge R620 | OK | 16 | | Dell PowerEdge R710 | OK | 17 | | Dell PowerEdge R720 | OK | 18 | | Dell PowerEdge R720xd | OK | 19 | | Dell PowerVault NX3000 | OK | 20 | | Dell Cloud Server C1100 (Direct from Dell) | Not Supported | 21 | | Dell Cloud Server C1100 CS24-TY (OEM Custom Build) | Not Supported | 22 | 23 | If you have tested this image on other hardware, please share your results in the form of a GitHub issue or Pull Request, and I'll update the list! 24 | 25 | > Note: if you get the error `USB is not enabled. Please enable USB and try update again`, you should make sure the `usb-storage` kernel module is loaded on the host machine. You can check this with `lsmod | grep usb-storage` and load it with `modprobe usb-storage`. To make this change permanent, add it to your system's auto-loaded modules (for example, on Debian/Ubuntu: `echo usb-storage > /etc/modules-load.d/usb-storage.conf`). 26 | 27 | # Firmware Upgrade 28 | To upgrade you Dell's server firmware from Dell Global repository, simply run: 29 | 30 | `docker run --rm -ti --privileged --net=host -v /dev:/dev kamermans/docker-openmanage dsu` 31 | 32 | You are presented with an interactive text interface where you can select the updates that you would like to apply based on what is available for your Dell server. 33 | 34 | > Note: I was able to obtain some of the functionality of this container by reverse-engineering the Dell DSET Live ISO Image. I have also created a Docker-based version of DSET, which is available in Docker Hub as [kamermans/dell-dset](https://hub.docker.com/r/kamermans/dell-dset/). This image has only one layer and no Dockerfile as it is a verbatim copy of the OS filesystem from the Dell ISO image. This image is tagged to match the ISO version, so `kamermans/dell-dset:SLI22_A00` matches the Dell ISO `SLI22_A00`. 35 | 36 | # Server Administrator 37 | This is losely based on the OpenManage container that Dell created. Notably, this image includes SNMP support and out-of-the box support for registration in OpenManage Essentials. 38 | Base Project: https://github.com/jose-delarosa/docker-images/tree/master/openmanage81 39 | 40 | The easiest way to get up and running is to download the standalone startup script, `openmanage.sh` 41 | from https://raw.githubusercontent.com/kamermans/docker-openmanage/master/openmanage.sh 42 | 43 | ``` 44 | # curl -sSL https://raw.githubusercontent.com/kamermans/docker-openmanage/master/openmanage.sh > ./openmanage.sh 45 | # chmod +x openmanage.sh 46 | # ./openmanage.sh 47 | 48 | OpenManage Server Administrator in a Docker Container 49 | 50 | Usage: ./openmanage.sh [snmp_community] [snmp_trap_dest] 51 | snmp_community The SNMP community string to use (default: public) 52 | snmp_trap_dest The SNMP trap destination - this is normally the IP 53 | or hostname to the OpenManage Essentials server 54 | (default: 192.168.0.1) 55 | 56 | Note that OpenManage Server Administrator will still work without 57 | either arguments, but will not be detected by OpenManage Essentials. 58 | ``` 59 | 60 | From here you can start the container (the image will be downloaded from Docker Hub the first time) as well as 61 | stop, restart and check the status of it. You can also download or update the image by running `./openmanage update` 62 | 63 | To connect it to OpenManage Essentials, you'll need to pass the `snmp_community` and `snmp_trap_dest` arguments 64 | so OpenManage Server Administrator knows how to connect to it. 65 | 66 | Note that this container uses the Docker options `--privileged`, `-v /dev:/dev` and `--net=host` in order to access your server 67 | hardware and correctly report the network configuration. 68 | 69 | If you are hesitent to download and run a bash script from some random site on the internet, and you can't understand 70 | my bash code, I would urge you to learn bash, then continue :) 71 | 72 | If you choose not to learn bash, here's how to run the container without the init script: 73 | 74 | docker run -d -P \ 75 | --name="openmanage" \ 76 | --privileged \ 77 | --net="host" \ 78 | -v /lib/modules/$(uname -r):/lib/modules/$(uname -r):ro \ 79 | -v /dev:/dev \ 80 | -e "SNMP_COMMUNITY=snmp_community" \ 81 | -e "SNMP_TRAP_DEST=snmp_trap_dest" \ 82 | kamermans/docker-openmanage 83 | 84 | Please feel free to browse the source code over at the GitHub repo: 85 | https://github.com/kamermans/docker-openmanage 86 | 87 | Special thanks to the following people for their contributions: 88 | - maltris 89 | - Shane StClair 90 | - Martin Taheri 91 | - Jose De la Rosa 92 | 93 | And a big thanks to Dell for sharing the original container with the open source community! 94 | -------------------------------------------------------------------------------- /openmanage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | usage() { 4 | cat << EOF 5 | OpenManage Server Administrator in a Docker Container 6 | 7 | Usage: ./$(basename $0) [snmp_community] [snmp_trap_dest] 8 | snmp_community The SNMP community string to use (default: public) 9 | snmp_trap_dest The SNMP trap destination - this is normally the IP 10 | or hostname to the OpenManage Essentials server 11 | (default: 192.168.0.1) 12 | 13 | Note that OpenManage Server Administrator will still work without 14 | either arguments, but will not be detected by OpenManage Essentials. 15 | EOF 16 | exit 1 17 | } 18 | 19 | start() { 20 | case "$(status)" in 21 | Running) 22 | echo "Error: Container '$CONTAINER_NAME' is already running" 23 | exit 1 24 | ;; 25 | Stopped) 26 | echo "Removing stopped container" 27 | docker rm -fv "$CONTAINER_NAME" 28 | ;; 29 | esac 30 | 31 | docker run -d \ 32 | --name="$CONTAINER_NAME" \ 33 | --privileged \ 34 | --net="host" \ 35 | -v /lib/modules/$(uname -r):/lib/modules/$(uname -r) \ 36 | -v /dev:/dev \ 37 | -e "SNMP_COMMUNITY=$SNMP_COMMUNITY" \ 38 | -e "SNMP_TRAP_DEST=$SNMP_TRAP_DEST" \ 39 | $DOCKER_IMAGE 40 | } 41 | 42 | stop() { 43 | case "$(status)" in 44 | Running) 45 | echo "Stopping container" 46 | docker stop "$CONTAINER_NAME" 47 | echo "Removing stopped container" 48 | docker rm -fv "$CONTAINER_NAME" 49 | exit 1 50 | ;; 51 | Stopped) 52 | echo "Removing stopped container" 53 | docker rm -fv "$CONTAINER_NAME" 54 | ;; 55 | *) 56 | echo "Container is already stopped" 57 | ;; 58 | esac 59 | } 60 | 61 | status() { 62 | STATUS=$(docker inspect --format='{{ .State.Running }}' "$CONTAINER_NAME" 2>/dev/null || echo "Nonexistent") 63 | echo $STATUS | sed -e 's/true/Running/' -e 's/false/Stopped/g' 64 | } 65 | 66 | update() { 67 | echo "Updating openmanage.sh" 68 | curl -sSL https://raw.githubusercontent.com/kamermans/docker-openmanage/master/openmanage.sh > $0 69 | 70 | echo "Updating Docker image $DOCKER_IMAGE" 71 | docker pull "$DOCKER_IMAGE" 72 | } 73 | 74 | if [ "$#" -eq 0 ] || [ "x$1" = "x-h" ] || [ "x$1" = "x--help" ]; then 75 | usage 76 | fi 77 | 78 | ACTION="$1" 79 | SNMP_COMMUNITY=${2:-"public"} 80 | SNMP_TRAP_DEST=${3:-"192.168.0.1"} 81 | CONTAINER_NAME="openmanage" 82 | DOCKER_IMAGE="kamermans/docker-openmanage" 83 | 84 | case "$ACTION" in 85 | start) 86 | start 87 | ;; 88 | stop) 89 | stop 90 | ;; 91 | restart) 92 | stop && start 93 | ;; 94 | status) 95 | status 96 | ;; 97 | update) 98 | update 99 | ;; 100 | *) 101 | usage 102 | ;; 103 | esac 104 | -------------------------------------------------------------------------------- /resources/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Print system information, serial number, etc 4 | dmidecode -t1 5 | 6 | echo "Configuring SNMPD" 7 | 8 | if [ "x$SNMP_COMMUNITY" != "x" ]; then 9 | echo " Setting community to '$SNMP_COMMUNITY'" 10 | sed -i -E "s/^(com2sec notConfigUser.*|Trapsink.*) public.*\$/\1 $SNMP_COMMUNITY/g" /etc/snmp/snmpd.conf 11 | fi 12 | 13 | if [ "x$SNMP_TRAP_DEST" != "x" ]; then 14 | # Use the IP or resolve the hostname to an IP 15 | SNMP_TRAP_DEST=$(grep -P "^\d+\.\d+\.\d+\.\d+$" <<< "$SNMP_TRAP_DEST" || getent hosts "$SNMP_TRAP_DEST" | cut -d" " -f1) 16 | echo " Setting trap sink (destination) to '$SNMP_TRAP_DEST'" 17 | sed -i -E "s/^(Trapsink ).* (.*)\$/\1 $SNMP_TRAP_DEST \2/g" /etc/snmp/snmpd.conf 18 | fi 19 | 20 | # Start Dell services 21 | echo "Starting Dell services, this may take a few minutes..." 22 | systemctl start snmpd.service 23 | srvadmin-services.sh start 24 | srvadmin-services.sh status 25 | 26 | # Run any passed commands 27 | if [ "$#" -gt 0 ]; then 28 | # Use eval instead of exec so this script remains PID 1 29 | eval "$@" 30 | fi 31 | -------------------------------------------------------------------------------- /resources/snmpd.conf: -------------------------------------------------------------------------------- 1 | com2sec notConfigUser default public 2 | group notConfigGroup v1 notConfigUser 3 | group notConfigGroup v2c notConfigUser 4 | view all included .1 5 | access notConfigGroup "" any noauth exact all none none 6 | syslocation Unknown (edit /etc/snmp/snmpd.conf) 7 | syscontact Root (configure /etc/snmp/snmp.local.conf) 8 | dontLogTCPWrappersConnects yes 9 | smuxpeer .1.3.6.1.4.1.674.10892.1 10 | Trapsink 192.168.0.1 public 11 | -------------------------------------------------------------------------------- /resources/systemctl.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | from __future__ import print_function 3 | 4 | __copyright__ = "(C) 2016-2019 Guido U. Draheim, licensed under the EUPL" 5 | __version__ = "1.4.3147" 6 | 7 | import logging 8 | logg = logging.getLogger("systemctl") 9 | 10 | import re 11 | import fnmatch 12 | import shlex 13 | import collections 14 | import errno 15 | import os 16 | import sys 17 | import signal 18 | import time 19 | import socket 20 | import datetime 21 | import fcntl 22 | 23 | if sys.version[0] == '2': 24 | string_types = basestring 25 | BlockingIOError = IOError 26 | else: 27 | string_types = str 28 | xrange = range 29 | 30 | COVERAGE = os.environ.get("SYSTEMCTL_COVERAGE", "") 31 | DEBUG_AFTER = os.environ.get("SYSTEMCTL_DEBUG_AFTER", "") or False 32 | EXIT_WHEN_NO_MORE_PROCS = os.environ.get("SYSTEMCTL_EXIT_WHEN_NO_MORE_PROCS", "") or False 33 | EXIT_WHEN_NO_MORE_SERVICES = os.environ.get("SYSTEMCTL_EXIT_WHEN_NO_MORE_SERVICES", "") or False 34 | 35 | # defaults for options 36 | _extra_vars = [] 37 | _force = False 38 | _full = False 39 | _now = False 40 | _no_legend = False 41 | _no_ask_password = False 42 | _preset_mode = "all" 43 | _quiet = False 44 | _root = "" 45 | _unit_type = None 46 | _unit_state = None 47 | _unit_property = None 48 | _show_all = False 49 | _user_mode = False 50 | 51 | # common default paths 52 | _default_target = "multi-user.target" 53 | _system_folder1 = "/etc/systemd/system" 54 | _system_folder2 = "/var/run/systemd/system" 55 | _system_folder3 = "/usr/lib/systemd/system" 56 | _system_folder4 = "/lib/systemd/system" 57 | _system_folder9 = None 58 | _user_folder1 = "~/.config/systemd/user" 59 | _user_folder2 = "/etc/systemd/user" 60 | _user_folder3 = "~.local/share/systemd/user" 61 | _user_folder4 = "/usr/lib/systemd/user" 62 | _user_folder9 = None 63 | _init_folder1 = "/etc/init.d" 64 | _init_folder2 = "/var/run/init.d" 65 | _init_folder9 = None 66 | _preset_folder1 = "/etc/systemd/system-preset" 67 | _preset_folder2 = "/var/run/systemd/system-preset" 68 | _preset_folder3 = "/usr/lib/systemd/system-preset" 69 | _preset_folder4 = "/lib/systemd/system-preset" 70 | _preset_folder9 = None 71 | 72 | SystemCompatibilityVersion = 219 73 | EpsilonTime = 0.1 74 | MinimumYield = 0.5 75 | MinimumTimeoutStartSec = 4 76 | MinimumTimeoutStopSec = 4 77 | DefaultTimeoutStartSec = int(os.environ.get("SYSTEMCTL_TIMEOUT_START_SEC", 90)) # official value 78 | DefaultTimeoutStopSec = int(os.environ.get("SYSTEMCTL_TIMEOUT_STOP_SEC", 90)) # official value 79 | DefaultMaximumTimeout = int(os.environ.get("SYSTEMCTL_MAXIMUM_TIMEOUT", 200)) # overrides all other 80 | InitLoopSleep = int(os.environ.get("SYSTEMCTL_INITLOOP", 5)) 81 | ProcMaxDepth = 100 82 | MaxLockWait = None # equals DefaultMaximumTimeout 83 | DefaultPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 84 | ResetLocale = ["LANG", "LANGUAGE", "LC_CTYPE", "LC_NUMERIC", "LC_TIME", "LC_COLLATE", "LC_MONETARY", 85 | "LC_MESSAGES", "LC_PAPER", "LC_NAME", "LC_ADDRESS", "LC_TELEPHONE", "LC_MEASUREMENT", 86 | "LC_IDENTIFICATION", "LC_ALL"] 87 | 88 | # The systemd default is NOTIFY_SOCKET="/var/run/systemd/notify" 89 | _notify_socket_folder = "/var/run/systemd" # alias /run/systemd 90 | _pid_file_folder = "/var/run" 91 | _journal_log_folder = "/var/log/journal" 92 | 93 | _systemctl_debug_log = "/var/log/systemctl.debug.log" 94 | _systemctl_extra_log = "/var/log/systemctl.log" 95 | 96 | _default_targets = [ "poweroff.target", "rescue.target", "sysinit.target", "basic.target", "multi-user.target", "graphical.target", "reboot.target" ] 97 | _feature_targets = [ "network.target", "remote-fs.target", "local-fs.target", "timers.target", "nfs-client.target" ] 98 | _all_common_targets = [ "default.target" ] + _default_targets + _feature_targets 99 | 100 | # inside a docker we pretend the following 101 | _all_common_enabled = [ "default.target", "multi-user.target", "remote-fs.target" ] 102 | _all_common_disabled = [ "graphical.target", "resue.target", "nfs-client.target" ] 103 | 104 | _runlevel_mappings = {} # the official list 105 | _runlevel_mappings["0"] = "poweroff.target" 106 | _runlevel_mappings["1"] = "rescue.target" 107 | _runlevel_mappings["2"] = "multi-user.target" 108 | _runlevel_mappings["3"] = "multi-user.target" 109 | _runlevel_mappings["4"] = "multi-user.target" 110 | _runlevel_mappings["5"] = "graphical.target" 111 | _runlevel_mappings["6"] = "reboot.target" 112 | 113 | _sysv_mappings = {} # by rule of thumb 114 | _sysv_mappings["$local_fs"] = "local-fs.target" 115 | _sysv_mappings["$network"] = "network.target" 116 | _sysv_mappings["$remote_fs"] = "remote-fs.target" 117 | _sysv_mappings["$timer"] = "timers.target" 118 | 119 | def shell_cmd(cmd): 120 | return " ".join(["'%s'" % part for part in cmd]) 121 | def to_int(value, default = 0): 122 | try: 123 | return int(value) 124 | except: 125 | return default 126 | def to_list(value): 127 | if isinstance(value, string_types): 128 | return [ value ] 129 | return value 130 | def unit_of(module): 131 | if "." not in module: 132 | return module + ".service" 133 | return module 134 | 135 | def os_path(root, path): 136 | if not root: 137 | return path 138 | if not path: 139 | return path 140 | while path.startswith(os.path.sep): 141 | path = path[1:] 142 | return os.path.join(root, path) 143 | 144 | def os_getlogin(): 145 | """ NOT using os.getlogin() """ 146 | import pwd 147 | return pwd.getpwuid(os.geteuid()).pw_name 148 | 149 | def get_runtime_dir(): 150 | explicit = os.environ.get("XDG_RUNTIME_DIR", "") 151 | if explicit: return explicit 152 | user = os_getlogin() 153 | return "/tmp/run-"+user 154 | 155 | def get_home(): 156 | explicit = os.environ.get("HOME", "") 157 | if explicit: return explicit 158 | return os.path.expanduser("~") 159 | 160 | def _var(path): 161 | """ assumes that the path starts with /var - and when 162 | in --user mode it is moved to /run/user/1001/run/ 163 | or as a fallback path to /tmp/run-{user}/ so that 164 | you may find /var/log in /tmp/run-{user}/log ..""" 165 | if not _user_mode: 166 | return path 167 | if path.startswith("/var"): 168 | runtime = get_runtime_dir() # $XDG_RUNTIME_DIR 169 | if not os.path.isdir(runtime): 170 | os.makedirs(runtime) 171 | os.chmod(runtime, 0o700) 172 | return re.sub("^(/var)?", get_runtime_dir(), path) 173 | return path 174 | 175 | 176 | def shutil_setuid(user = None, group = None): 177 | """ set fork-child uid/gid (returns pw-info env-settings)""" 178 | if group: 179 | import grp 180 | gid = grp.getgrnam(group).gr_gid 181 | os.setgid(gid) 182 | logg.debug("setgid %s '%s'", gid, group) 183 | if user: 184 | import pwd 185 | pw = pwd.getpwnam(user) 186 | if not group: 187 | gid = pw.pw_gid 188 | os.setgid(gid) 189 | logg.debug("setgid %s", gid) 190 | uid = pw.pw_uid 191 | os.setuid(uid) 192 | logg.debug("setuid %s '%s'", uid, user) 193 | home = pw.pw_dir 194 | shell = pw.pw_shell 195 | logname = pw.pw_name 196 | return { "USER": user, "LOGNAME": logname, "HOME": home, "SHELL": shell } 197 | return {} 198 | 199 | def shutil_truncate(filename): 200 | """ truncates the file (or creates a new empty file)""" 201 | filedir = os.path.dirname(filename) 202 | if not os.path.isdir(filedir): 203 | os.makedirs(filedir) 204 | f = open(filename, "w") 205 | f.write("") 206 | f.close() 207 | 208 | # http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid 209 | def pid_exists(pid): 210 | """Check whether pid exists in the current process table.""" 211 | if pid is None: 212 | return False 213 | return _pid_exists(int(pid)) 214 | def _pid_exists(pid): 215 | """Check whether pid exists in the current process table. 216 | UNIX only. 217 | """ 218 | if pid < 0: 219 | return False 220 | if pid == 0: 221 | # According to "man 2 kill" PID 0 refers to every process 222 | # in the process group of the calling process. 223 | # On certain systems 0 is a valid PID but we have no way 224 | # to know that in a portable fashion. 225 | raise ValueError('invalid PID 0') 226 | try: 227 | os.kill(pid, 0) 228 | except OSError as err: 229 | if err.errno == errno.ESRCH: 230 | # ESRCH == No such process 231 | return False 232 | elif err.errno == errno.EPERM: 233 | # EPERM clearly means there's a process to deny access to 234 | return True 235 | else: 236 | # According to "man 2 kill" possible error values are 237 | # (EINVAL, EPERM, ESRCH) 238 | raise 239 | else: 240 | return True 241 | def pid_zombie(pid): 242 | """ may be a pid exists but it is only a zombie """ 243 | if pid is None: 244 | return False 245 | return _pid_zombie(int(pid)) 246 | def _pid_zombie(pid): 247 | """ may be a pid exists but it is only a zombie """ 248 | if pid < 0: 249 | return False 250 | if pid == 0: 251 | # According to "man 2 kill" PID 0 refers to every process 252 | # in the process group of the calling process. 253 | # On certain systems 0 is a valid PID but we have no way 254 | # to know that in a portable fashion. 255 | raise ValueError('invalid PID 0') 256 | check = "/proc/%s/status" % pid 257 | try: 258 | for line in open(check): 259 | if line.startswith("State:"): 260 | return "Z" in line 261 | except IOError as e: 262 | if e.errno != errno.ENOENT: 263 | logg.error("%s (%s): %s", check, e.errno, e) 264 | return False 265 | return False 266 | 267 | def checkstatus(cmd): 268 | if cmd.startswith("-"): 269 | return False, cmd[1:] 270 | else: 271 | return True, cmd 272 | 273 | # https://github.com/phusion/baseimage-docker/blob/rel-0.9.16/image/bin/my_init 274 | def ignore_signals_and_raise_keyboard_interrupt(signame): 275 | signal.signal(signal.SIGTERM, signal.SIG_IGN) 276 | signal.signal(signal.SIGINT, signal.SIG_IGN) 277 | raise KeyboardInterrupt(signame) 278 | 279 | class SystemctlConfigParser: 280 | """ A *.service files has a structure similar to an *.ini file but it is 281 | actually not like it. Settings may occur multiple times in each section 282 | and they create an implicit list. In reality all the settings are 283 | globally uniqute, so that an 'environment' can be printed without 284 | adding prefixes. Settings are continued with a backslash at the end 285 | of the line. """ 286 | def __init__(self, defaults=None, dict_type=None, allow_no_value=False): 287 | self._defaults = defaults or {} 288 | self._dict_type = dict_type or collections.OrderedDict 289 | self._allow_no_value = allow_no_value 290 | self._conf = self._dict_type() 291 | self._files = [] 292 | def defaults(self): 293 | return self._defaults 294 | def sections(self): 295 | return list(self._conf.keys()) 296 | def add_section(self, section): 297 | if section not in self._conf: 298 | self._conf[section] = self._dict_type() 299 | def has_section(self, section): 300 | return section in self._conf 301 | def has_option(self, section, option): 302 | if section not in self._conf: 303 | return False 304 | return option in self._conf[section] 305 | def set(self, section, option, value): 306 | if section not in self._conf: 307 | self._conf[section] = self._dict_type() 308 | if option not in self._conf[section]: 309 | self._conf[section][option] = [ value ] 310 | else: 311 | self._conf[section][option].append(value) 312 | if value is None: 313 | self._conf[section][option] = [] 314 | def get(self, section, option, default = None, allow_no_value = False): 315 | allow_no_value = allow_no_value or self._allow_no_value 316 | if section not in self._conf: 317 | if default is not None: 318 | return default 319 | if allow_no_value: 320 | return None 321 | logg.warning("section {} does not exist".format(section)) 322 | logg.warning(" have {}".format(self.sections())) 323 | raise AttributeError("section {} does not exist".format(section)) 324 | if option not in self._conf[section]: 325 | if default is not None: 326 | return default 327 | if allow_no_value: 328 | return None 329 | raise AttributeError("option {} in {} does not exist".format(option, section)) 330 | if not self._conf[section][option]: # i.e. an empty list 331 | if default is not None: 332 | return default 333 | if allow_no_value: 334 | return None 335 | raise AttributeError("option {} in {} is None".format(option, section)) 336 | return self._conf[section][option][0] # the first line in the list of configs 337 | def getlist(self, section, option, default = None, allow_no_value = False): 338 | allow_no_value = allow_no_value or self._allow_no_value 339 | if section not in self._conf: 340 | if default is not None: 341 | return default 342 | if allow_no_value: 343 | return [] 344 | logg.warning("section {} does not exist".format(section)) 345 | logg.warning(" have {}".format(self.sections())) 346 | raise AttributeError("section {} does not exist".format(section)) 347 | if option not in self._conf[section]: 348 | if default is not None: 349 | return default 350 | if allow_no_value: 351 | return [] 352 | raise AttributeError("option {} in {} does not exist".format(option, section)) 353 | return self._conf[section][option] # returns a list, possibly empty 354 | def read(self, filename): 355 | return self.read_sysd(filename) 356 | def read_sysd(self, filename): 357 | initscript = False 358 | initinfo = False 359 | section = None 360 | nextline = False 361 | name, text = "", "" 362 | if os.path.isfile(filename): 363 | self._files.append(filename) 364 | for orig_line in open(filename): 365 | if nextline: 366 | text += orig_line 367 | if text.rstrip().endswith("\\") or text.rstrip().endswith("\\\n"): 368 | text = text.rstrip() + "\n" 369 | else: 370 | self.set(section, name, text) 371 | nextline = False 372 | continue 373 | line = orig_line.strip() 374 | if not line: 375 | continue 376 | if line.startswith("#"): 377 | continue 378 | if line.startswith(";"): 379 | continue 380 | if line.startswith(".include"): 381 | logg.error("the '.include' syntax is deprecated. Use x.service.d/ drop-in files!") 382 | includefile = re.sub(r'^\.include[ ]*', '', line).rstrip() 383 | if not os.path.isfile(includefile): 384 | raise Exception("tried to include file that doesn't exist: %s" % includefile) 385 | self.read_sysd(includefile) 386 | continue 387 | if line.startswith("["): 388 | x = line.find("]") 389 | if x > 0: 390 | section = line[1:x] 391 | self.add_section(section) 392 | continue 393 | m = re.match(r"(\w+) *=(.*)", line) 394 | if not m: 395 | logg.warning("bad ini line: %s", line) 396 | raise Exception("bad ini line") 397 | name, text = m.group(1), m.group(2).strip() 398 | if text.endswith("\\") or text.endswith("\\\n"): 399 | nextline = True 400 | text = text + "\n" 401 | else: 402 | # hint: an empty line shall reset the value-list 403 | self.set(section, name, text and text or None) 404 | def read_sysv(self, filename): 405 | """ an LSB header is scanned and converted to (almost) 406 | equivalent settings of a SystemD ini-style input """ 407 | initscript = False 408 | initinfo = False 409 | section = None 410 | if os.path.isfile(filename): 411 | self._files.append(filename) 412 | for orig_line in open(filename): 413 | line = orig_line.strip() 414 | if line.startswith("#"): 415 | if " BEGIN INIT INFO" in line: 416 | initinfo = True 417 | section = "init.d" 418 | if " END INIT INFO" in line: 419 | initinfo = False 420 | if initinfo: 421 | m = re.match(r"\S+\s*(\w[\w_-]*):(.*)", line) 422 | if m: 423 | key, val = m.group(1), m.group(2).strip() 424 | self.set(section, key, val) 425 | continue 426 | description = self.get("init.d", "Description", "") 427 | if description: 428 | self.set("Unit", "Description", description) 429 | check = self.get("init.d", "Required-Start","") 430 | if check: 431 | for item in check.split(" "): 432 | if item.strip() in _sysv_mappings: 433 | self.set("Unit", "Requires", _sysv_mappings[item.strip()]) 434 | provides = self.get("init.d", "Provides", "") 435 | if provides: 436 | self.set("Install", "Alias", provides) 437 | # if already in multi-user.target then start it there. 438 | runlevels = self.get("init.d", "Default-Start","") 439 | if runlevels: 440 | for item in runlevels.split(" "): 441 | if item.strip() in _runlevel_mappings: 442 | self.set("Install", "WantedBy", _runlevel_mappings[item.strip()]) 443 | self.set("Service", "Type", "sysv") 444 | def filenames(self): 445 | return self._files 446 | 447 | # UnitConfParser = ConfigParser.RawConfigParser 448 | UnitConfParser = SystemctlConfigParser 449 | 450 | class SystemctlConf: 451 | def __init__(self, data, module = None): 452 | self.data = data # UnitConfParser 453 | self.env = {} 454 | self.status = None 455 | self.masked = None 456 | self.module = module 457 | self.drop_in_files = {} 458 | def loaded(self): 459 | files = self.data.filenames() 460 | if self.masked: 461 | return "masked" 462 | if len(files): 463 | return "loaded" 464 | return "" 465 | def filename(self): 466 | """ returns the last filename that was parsed """ 467 | files = self.data.filenames() 468 | if files: 469 | return files[0] 470 | return None 471 | def overrides(self): 472 | """ drop-in files are loaded alphabetically by name, not by full path """ 473 | return [ self.drop_in_files[name] for name in sorted(self.drop_in_files) ] 474 | def name(self): 475 | """ the unit id or defaults to the file name """ 476 | name = self.module or "" 477 | filename = self.filename() 478 | if filename: 479 | name = os.path.basename(filename) 480 | return self.get("Unit", "Id", name) 481 | def set(self, section, name, value): 482 | return self.data.set(section, name, value) 483 | def get(self, section, name, default, allow_no_value = False): 484 | return self.data.get(section, name, default, allow_no_value) 485 | def getlist(self, section, name, default = None, allow_no_value = False): 486 | return self.data.getlist(section, name, default or [], allow_no_value) 487 | def getbool(self, section, name, default = None): 488 | value = self.data.get(section, name, default or "no") 489 | if value: 490 | if value[0] in "TtYy123456789": 491 | return True 492 | return False 493 | 494 | class PresetFile: 495 | def __init__(self): 496 | self._files = [] 497 | self._lines = [] 498 | def filename(self): 499 | """ returns the last filename that was parsed """ 500 | if self._files: 501 | return self._files[-1] 502 | return None 503 | def read(self, filename): 504 | self._files.append(filename) 505 | for line in open(filename): 506 | self._lines.append(line.strip()) 507 | return self 508 | def get_preset(self, unit): 509 | for line in self._lines: 510 | m = re.match(r"(enable|disable)\s+(\S+)", line) 511 | if m: 512 | status, pattern = m.group(1), m.group(2) 513 | if fnmatch.fnmatchcase(unit, pattern): 514 | logg.debug("%s %s => %s [%s]", status, pattern, unit, self.filename()) 515 | return status 516 | return None 517 | 518 | ## with waitlock(conf): self.start() 519 | class waitlock: 520 | def __init__(self, conf): 521 | self.conf = conf # currently unused 522 | self.opened = None 523 | self.lockfolder = os_path(_root, _var(_notify_socket_folder)) 524 | try: 525 | folder = self.lockfolder 526 | if not os.path.isdir(folder): 527 | os.makedirs(folder) 528 | except Exception as e: 529 | logg.warning("oops, %s", e) 530 | def lockfile(self): 531 | unit = "" 532 | if self.conf: 533 | unit = self.conf.name() 534 | return os.path.join(self.lockfolder, str(unit or "global") + ".lock") 535 | def __enter__(self): 536 | try: 537 | lockfile = self.lockfile() 538 | lockname = os.path.basename(lockfile) 539 | self.opened = os.open(lockfile, os.O_RDWR | os.O_CREAT, 0o600) 540 | for attempt in xrange(int(MaxLockWait or DefaultMaximumTimeout)): 541 | try: 542 | logg.debug("[%s] %s. trying %s _______ ", os.getpid(), attempt, lockname) 543 | fcntl.flock(self.opened, fcntl.LOCK_EX | fcntl.LOCK_NB) 544 | st = os.fstat(self.opened) 545 | if not st.st_nlink: 546 | logg.debug("[%s] %s. %s got deleted, trying again", os.getpid(), attempt, lockname) 547 | os.close(self.opened) 548 | self.opened = os.open(lockfile, os.O_RDWR | os.O_CREAT, 0o600) 549 | continue 550 | content = "{ 'systemctl': %s, 'lock': '%s' }\n" % (os.getpid(), lockname) 551 | os.write(self.opened, content.encode("utf-8")) 552 | logg.debug("[%s] %s. holding lock on %s", os.getpid(), attempt, lockname) 553 | return True 554 | except BlockingIOError as e: 555 | whom = os.read(self.opened, 4096) 556 | os.lseek(self.opened, 0, os.SEEK_SET) 557 | logg.info("[%s] %s. systemctl locked by %s", os.getpid(), attempt, whom.rstrip()) 558 | time.sleep(1) # until MaxLockWait 559 | continue 560 | logg.error("[%s] not able to get the lock to %s", os.getpid(), lockname) 561 | except Exception as e: 562 | logg.warning("[%s] oops %s, %s", os.getpid(), str(type(e)), e) 563 | #TODO# raise Exception("no lock for %s", self.unit or "global") 564 | return False 565 | def __exit__(self, type, value, traceback): 566 | try: 567 | os.lseek(self.opened, 0, os.SEEK_SET) 568 | os.ftruncate(self.opened, 0) 569 | if "removelockfile" in COVERAGE: # actually an optional implementation 570 | lockfile = self.lockfile() 571 | lockname = os.path.basename(lockfile) 572 | os.unlink(lockfile) # ino is kept allocated because opened by this process 573 | logg.debug("[%s] lockfile removed for %s", os.getpid(), lockname) 574 | fcntl.flock(self.opened, fcntl.LOCK_UN) 575 | os.close(self.opened) # implies an unlock but that has happend like 6 seconds later 576 | self.opened = None 577 | except Exception as e: 578 | logg.warning("oops, %s", e) 579 | 580 | def must_have_failed(waitpid, cmd): 581 | # found to be needed on ubuntu:16.04 to match test result from ubuntu:18.04 and other distros 582 | # .... I have tracked it down that python's os.waitpid() returns an exitcode==0 even when the 583 | # .... underlying process has actually failed with an exitcode<>0. It is unknown where that 584 | # .... bug comes from but it seems a bit serious to trash some very basic unix functionality. 585 | # .... Essentially a parent process does not get the correct exitcode from its own children. 586 | if cmd and cmd[0] == "/bin/kill": 587 | pid = None 588 | for arg in cmd[1:]: 589 | if not arg.startswith("-"): 590 | pid = arg 591 | if pid is None: # unknown $MAINPID 592 | if not waitpid.returncode: 593 | logg.error("waitpid %s did return %s => correcting as 11", cmd, waitpid.returncode) 594 | waitpidNEW = collections.namedtuple("waitpidNEW", ["pid", "returncode", "signal" ]) 595 | waitpid = waitpidNEW(waitpid.pid, 11, waitpid.signal) 596 | return waitpid 597 | 598 | def subprocess_waitpid(pid): 599 | waitpid = collections.namedtuple("waitpid", ["pid", "returncode", "signal" ]) 600 | run_pid, run_stat = os.waitpid(pid, 0) 601 | return waitpid(run_pid, os.WEXITSTATUS(run_stat), os.WTERMSIG(run_stat)) 602 | def subprocess_testpid(pid): 603 | testpid = collections.namedtuple("testpid", ["pid", "returncode", "signal" ]) 604 | run_pid, run_stat = os.waitpid(pid, os.WNOHANG) 605 | if run_pid: 606 | return testpid(run_pid, os.WEXITSTATUS(run_stat), os.WTERMSIG(run_stat)) 607 | else: 608 | return testpid(pid, None, 0) 609 | 610 | def parse_unit(name): # -> object(prefix, instance, suffix, ...., name, component) 611 | unit_name, suffix = name, "" 612 | has_suffix = name.rfind(".") 613 | if has_suffix > 0: 614 | unit_name = name[:has_suffix] 615 | suffix = name[has_suffix+1:] 616 | prefix, instance = unit_name, "" 617 | has_instance = unit_name.find("@") 618 | if has_instance > 0: 619 | prefix = unit_name[:has_instance] 620 | instance = unit_name[has_instance+1:] 621 | component = "" 622 | has_component = prefix.rfind("-") 623 | if has_component > 0: 624 | component = prefix[has_component+1:] 625 | UnitName = collections.namedtuple("UnitName", ["name", "prefix", "instance", "suffix", "component" ]) 626 | return UnitName(name, prefix, instance, suffix, component) 627 | 628 | def time_to_seconds(text, maximum = None): 629 | if maximum is None: 630 | maximum = DefaultMaximumTimeout 631 | value = 0 632 | for part in str(text).split(" "): 633 | item = part.strip() 634 | if item == "infinity": 635 | return maximum 636 | if item.endswith("m"): 637 | try: value += 60 * int(item[:-1]) 638 | except: pass # pragma: no cover 639 | if item.endswith("min"): 640 | try: value += 60 * int(item[:-3]) 641 | except: pass # pragma: no cover 642 | elif item.endswith("ms"): 643 | try: value += int(item[:-2]) / 1000. 644 | except: pass # pragma: no cover 645 | elif item.endswith("s"): 646 | try: value += int(item[:-1]) 647 | except: pass # pragma: no cover 648 | elif item: 649 | try: value += int(item) 650 | except: pass # pragma: no cover 651 | if value > maximum: 652 | return maximum 653 | if not value: 654 | return 1 655 | return value 656 | def seconds_to_time(seconds): 657 | seconds = float(seconds) 658 | mins = int(int(seconds) / 60) 659 | secs = int(int(seconds) - (mins * 60)) 660 | msecs = int(int(seconds * 1000) - (secs * 1000 + mins * 60000)) 661 | if mins and secs and msecs: 662 | return "%smin %ss %sms" % (mins, secs, msecs) 663 | elif mins and secs: 664 | return "%smin %ss" % (mins, secs) 665 | elif secs and msecs: 666 | return "%ss %sms" % (secs, msecs) 667 | elif mins and msecs: 668 | return "%smin %sms" % (mins, msecs) 669 | elif mins: 670 | return "%smin" % (mins) 671 | else: 672 | return "%ss" % (secs) 673 | 674 | def getBefore(conf): 675 | result = [] 676 | beforelist = conf.getlist("Unit", "Before", []) 677 | for befores in beforelist: 678 | for before in befores.split(" "): 679 | name = before.strip() 680 | if name and name not in result: 681 | result.append(name) 682 | return result 683 | 684 | def getAfter(conf): 685 | result = [] 686 | afterlist = conf.getlist("Unit", "After", []) 687 | for afters in afterlist: 688 | for after in afters.split(" "): 689 | name = after.strip() 690 | if name and name not in result: 691 | result.append(name) 692 | return result 693 | 694 | def compareAfter(confA, confB): 695 | idA = confA.name() 696 | idB = confB.name() 697 | for after in getAfter(confA): 698 | if after == idB: 699 | logg.debug("%s After %s", idA, idB) 700 | return -1 701 | for after in getAfter(confB): 702 | if after == idA: 703 | logg.debug("%s After %s", idB, idA) 704 | return 1 705 | for before in getBefore(confA): 706 | if before == idB: 707 | logg.debug("%s Before %s", idA, idB) 708 | return 1 709 | for before in getBefore(confB): 710 | if before == idA: 711 | logg.debug("%s Before %s", idB, idA) 712 | return -1 713 | return 0 714 | 715 | def sortedAfter(conflist, cmp = compareAfter): 716 | # the normal sorted() does only look at two items 717 | # so if "A after C" and a list [A, B, C] then 718 | # it will see "A = B" and "B = C" assuming that 719 | # "A = C" and the list is already sorted. 720 | # 721 | # To make a totalsorted we have to create a marker 722 | # that informs sorted() that also B has a relation. 723 | # It only works when 'after' has a direction, so 724 | # anything without 'before' is a 'after'. In that 725 | # case we find that "B after C". 726 | class SortTuple: 727 | def __init__(self, rank, conf): 728 | self.rank = rank 729 | self.conf = conf 730 | sortlist = [ SortTuple(0, conf) for conf in conflist] 731 | for check in xrange(len(sortlist)): # maxrank = len(sortlist) 732 | changed = 0 733 | for A in xrange(len(sortlist)): 734 | for B in xrange(len(sortlist)): 735 | if A != B: 736 | itemA = sortlist[A] 737 | itemB = sortlist[B] 738 | before = compareAfter(itemA.conf, itemB.conf) 739 | if before > 0 and itemA.rank <= itemB.rank: 740 | if DEBUG_AFTER: # pragma: no cover 741 | logg.info(" %-30s before %s", itemA.conf.name(), itemB.conf.name()) 742 | itemA.rank = itemB.rank + 1 743 | changed += 1 744 | if before < 0 and itemB.rank <= itemA.rank: 745 | if DEBUG_AFTER: # pragma: no cover 746 | logg.info(" %-30s before %s", itemB.conf.name(), itemA.conf.name()) 747 | itemB.rank = itemA.rank + 1 748 | changed += 1 749 | if not changed: 750 | if DEBUG_AFTER: # pragma: no cover 751 | logg.info("done in check %s of %s", check, len(sortlist)) 752 | break 753 | # because Requires is almost always the same as the After clauses 754 | # we are mostly done in round 1 as the list is in required order 755 | for conf in conflist: 756 | if DEBUG_AFTER: # pragma: no cover 757 | logg.debug(".. %s", conf.name()) 758 | for item in sortlist: 759 | if DEBUG_AFTER: # pragma: no cover 760 | logg.info("(%s) %s", item.rank, item.conf.name()) 761 | sortedlist = sorted(sortlist, key = lambda item: -item.rank) 762 | for item in sortedlist: 763 | if DEBUG_AFTER: # pragma: no cover 764 | logg.info("[%s] %s", item.rank, item.conf.name()) 765 | return [ item.conf for item in sortedlist ] 766 | 767 | class Systemctl: 768 | def __init__(self): 769 | # from command line options or the defaults 770 | self._extra_vars = _extra_vars 771 | self._force = _force 772 | self._full = _full 773 | self._init = _init 774 | self._no_ask_password = _no_ask_password 775 | self._no_legend = _no_legend 776 | self._now = _now 777 | self._preset_mode = _preset_mode 778 | self._quiet = _quiet 779 | self._root = _root 780 | self._show_all = _show_all 781 | self._unit_property = _unit_property 782 | self._unit_state = _unit_state 783 | self._unit_type = _unit_type 784 | # some common constants that may be changed 785 | self._systemd_version = SystemCompatibilityVersion 786 | self._pid_file_folder = _pid_file_folder 787 | self._journal_log_folder = _journal_log_folder 788 | # and the actual internal runtime state 789 | self._loaded_file_sysv = {} # /etc/init.d/name => config data 790 | self._loaded_file_sysd = {} # /etc/systemd/system/name.service => config data 791 | self._file_for_unit_sysv = None # name.service => /etc/init.d/name 792 | self._file_for_unit_sysd = None # name.service => /etc/systemd/system/name.service 793 | self._preset_file_list = None # /etc/systemd/system-preset/* => file content 794 | self._default_target = _default_target 795 | self.exit_when_no_more_procs = EXIT_WHEN_NO_MORE_PROCS or False 796 | self.exit_when_no_more_services = EXIT_WHEN_NO_MORE_SERVICES or False 797 | self._user_mode = _user_mode 798 | self._user_getlogin = os_getlogin() 799 | self._log_file = {} # init-loop 800 | self._log_hold = {} # init-loop 801 | def user(self): 802 | return self._user_getlogin 803 | def user_mode(self): 804 | return self._user_mode 805 | def user_folder(self): 806 | for folder in self.user_folders(): 807 | if folder: return folder 808 | raise Exception("did not find any systemd/user folder") 809 | def system_folder(self): 810 | for folder in self.system_folders(): 811 | if folder: return folder 812 | raise Exception("did not find any systemd/system folder") 813 | def init_folders(self): 814 | if _init_folder1: yield _init_folder1 815 | if _init_folder2: yield _init_folder2 816 | if _init_folder9: yield _init_folder9 817 | def preset_folders(self): 818 | if _preset_folder1: yield _preset_folder1 819 | if _preset_folder2: yield _preset_folder2 820 | if _preset_folder3: yield _preset_folder3 821 | if _preset_folder4: yield _preset_folder4 822 | if _preset_folder9: yield _preset_folder9 823 | def user_folders(self): 824 | if _user_folder1: yield os.path.expanduser(_user_folder1) 825 | if _user_folder2: yield os.path.expanduser(_user_folder2) 826 | if _user_folder3: yield os.path.expanduser(_user_folder3) 827 | if _user_folder4: yield os.path.expanduser(_user_folder4) 828 | if _user_folder9: yield os.path.expanduser(_user_folder9) 829 | def system_folders(self): 830 | if _system_folder1: yield _system_folder1 831 | if _system_folder2: yield _system_folder2 832 | if _system_folder3: yield _system_folder3 833 | if _system_folder4: yield _system_folder4 834 | if _system_folder9: yield _system_folder9 835 | def sysd_folders(self): 836 | """ if --user then these folders are preferred """ 837 | if self.user_mode(): 838 | for folder in self.user_folders(): 839 | yield folder 840 | if True: 841 | for folder in self.system_folders(): 842 | yield folder 843 | def scan_unit_sysd_files(self, module = None): # -> [ unit-names,... ] 844 | """ reads all unit files, returns the first filename for the unit given """ 845 | if self._file_for_unit_sysd is None: 846 | self._file_for_unit_sysd = {} 847 | for folder in self.sysd_folders(): 848 | if not folder: 849 | continue 850 | if self._root: 851 | folder = os_path(self._root, folder) 852 | if not os.path.isdir(folder): 853 | continue 854 | for name in os.listdir(folder): 855 | path = os.path.join(folder, name) 856 | if os.path.isdir(path): 857 | continue 858 | service_name = name 859 | if service_name not in self._file_for_unit_sysd: 860 | self._file_for_unit_sysd[service_name] = path 861 | logg.debug("found %s sysd files", len(self._file_for_unit_sysd)) 862 | return list(self._file_for_unit_sysd.keys()) 863 | def scan_unit_sysv_files(self, module = None): # -> [ unit-names,... ] 864 | """ reads all init.d files, returns the first filename when unit is a '.service' """ 865 | if self._file_for_unit_sysv is None: 866 | self._file_for_unit_sysv = {} 867 | for folder in self.init_folders(): 868 | if not folder: 869 | continue 870 | if self._root: 871 | folder = os_path(self._root, folder) 872 | if not os.path.isdir(folder): 873 | continue 874 | for name in os.listdir(folder): 875 | path = os.path.join(folder, name) 876 | if os.path.isdir(path): 877 | continue 878 | service_name = name + ".service" # simulate systemd 879 | if service_name not in self._file_for_unit_sysv: 880 | self._file_for_unit_sysv[service_name] = path 881 | logg.debug("found %s sysv files", len(self._file_for_unit_sysv)) 882 | return list(self._file_for_unit_sysv.keys()) 883 | def unit_sysd_file(self, module = None): # -> filename? 884 | """ file path for the given module (systemd) """ 885 | self.scan_unit_sysd_files() 886 | if module and module in self._file_for_unit_sysd: 887 | return self._file_for_unit_sysd[module] 888 | if module and unit_of(module) in self._file_for_unit_sysd: 889 | return self._file_for_unit_sysd[unit_of(module)] 890 | return None 891 | def unit_sysv_file(self, module = None): # -> filename? 892 | """ file path for the given module (sysv) """ 893 | self.scan_unit_sysv_files() 894 | if module and module in self._file_for_unit_sysv: 895 | return self._file_for_unit_sysv[module] 896 | if module and unit_of(module) in self._file_for_unit_sysv: 897 | return self._file_for_unit_sysv[unit_of(module)] 898 | return None 899 | def unit_file(self, module = None): # -> filename? 900 | """ file path for the given module (sysv or systemd) """ 901 | path = self.unit_sysd_file(module) 902 | if path is not None: return path 903 | path = self.unit_sysv_file(module) 904 | if path is not None: return path 905 | return None 906 | def is_sysv_file(self, filename): 907 | """ for routines that have a special treatment for init.d services """ 908 | self.unit_file() # scan all 909 | if not filename: return None 910 | if filename in self._file_for_unit_sysd.values(): return False 911 | if filename in self._file_for_unit_sysv.values(): return True 912 | return None # not True 913 | def is_user_conf(self, conf): 914 | if not conf: 915 | return False # no such conf >> ignored 916 | filename = conf.filename() 917 | if filename and "/user/" in filename: 918 | return True 919 | return False 920 | def not_user_conf(self, conf): 921 | """ conf can not be started as user service (when --user)""" 922 | if not conf: 923 | return True # no such conf >> ignored 924 | if not self.user_mode(): 925 | logg.debug("%s no --user mode >> accept", conf.filename()) 926 | return False 927 | if self.is_user_conf(conf): 928 | logg.debug("%s is /user/ conf >> accept", conf.filename()) 929 | return False 930 | # to allow for 'docker run -u user' with system services 931 | user = self.expand_special(conf.get("Service", "User", ""), conf) 932 | if user and user == self.user(): 933 | logg.debug("%s with User=%s >> accept", conf.filename(), user) 934 | return False 935 | return True 936 | def find_drop_in_files(self, unit): 937 | """ search for some.service.d/extra.conf files """ 938 | result = {} 939 | basename_d = unit + ".d" 940 | for folder in self.sysd_folders(): 941 | if not folder: 942 | continue 943 | if self._root: 944 | folder = os_path(self._root, folder) 945 | override_d = os_path(folder, basename_d) 946 | if not os.path.isdir(override_d): 947 | continue 948 | for name in os.listdir(override_d): 949 | path = os.path.join(override_d, name) 950 | if os.path.isdir(path): 951 | continue 952 | if not path.endswith(".conf"): 953 | continue 954 | if name not in result: 955 | result[name] = path 956 | return result 957 | def load_sysd_unit_conf(self, module): # -> conf? 958 | """ read the unit file with a UnitConfParser (systemd) """ 959 | path = self.unit_sysd_file(module) 960 | if not path: return None 961 | if path in self._loaded_file_sysd: 962 | return self._loaded_file_sysd[path] 963 | masked = None 964 | if os.path.islink(path) and os.readlink(path).startswith("/dev"): 965 | masked = os.readlink(path) 966 | drop_in_files = {} 967 | data = UnitConfParser() 968 | if not masked: 969 | data.read_sysd(path) 970 | drop_in_files = self.find_drop_in_files(os.path.basename(path)) 971 | # load in alphabetic order, irrespective of location 972 | for name in sorted(drop_in_files): 973 | path = drop_in_files[name] 974 | data.read_sysd(path) 975 | conf = SystemctlConf(data, module) 976 | conf.masked = masked 977 | conf.drop_in_files = drop_in_files 978 | self._loaded_file_sysd[path] = conf 979 | return conf 980 | def load_sysv_unit_conf(self, module): # -> conf? 981 | """ read the unit file with a UnitConfParser (sysv) """ 982 | path = self.unit_sysv_file(module) 983 | if not path: return None 984 | if path in self._loaded_file_sysv: 985 | return self._loaded_file_sysv[path] 986 | data = UnitConfParser() 987 | data.read_sysv(path) 988 | conf = SystemctlConf(data, module) 989 | self._loaded_file_sysv[path] = conf 990 | return conf 991 | def load_unit_conf(self, module): # -> conf | None(not-found) 992 | """ read the unit file with a UnitConfParser (sysv or systemd) """ 993 | try: 994 | conf = self.load_sysd_unit_conf(module) 995 | if conf is not None: 996 | return conf 997 | conf = self.load_sysv_unit_conf(module) 998 | if conf is not None: 999 | return conf 1000 | except Exception as e: 1001 | logg.warning("%s not loaded: %s", module, e) 1002 | return None 1003 | def default_unit_conf(self, module): # -> conf 1004 | """ a unit conf that can be printed to the user where 1005 | attributes are empty and loaded() is False """ 1006 | data = UnitConfParser() 1007 | data.set("Unit","Id", module) 1008 | data.set("Unit", "Names", module) 1009 | data.set("Unit", "Description", "NOT-FOUND "+module) 1010 | # assert(not data.loaded()) 1011 | return SystemctlConf(data, module) 1012 | def get_unit_conf(self, module): # -> conf (conf | default-conf) 1013 | """ accept that a unit does not exist 1014 | and return a unit conf that says 'not-loaded' """ 1015 | conf = self.load_unit_conf(module) 1016 | if conf is not None: 1017 | return conf 1018 | return self.default_unit_conf(module) 1019 | def match_sysd_units(self, modules = None, suffix=".service"): # -> generate[ unit ] 1020 | """ make a file glob on all known units (systemd areas). 1021 | It returns all modules if no modules pattern were given. 1022 | Also a single string as one module pattern may be given. """ 1023 | modules = to_list(modules) 1024 | self.scan_unit_sysd_files() 1025 | for item in sorted(self._file_for_unit_sysd.keys()): 1026 | if not modules: 1027 | yield item 1028 | elif [ module for module in modules if fnmatch.fnmatchcase(item, module) ]: 1029 | yield item 1030 | elif [ module for module in modules if module+suffix == item ]: 1031 | yield item 1032 | def match_sysv_units(self, modules = None, suffix=".service"): # -> generate[ unit ] 1033 | """ make a file glob on all known units (sysv areas). 1034 | It returns all modules if no modules pattern were given. 1035 | Also a single string as one module pattern may be given. """ 1036 | modules = to_list(modules) 1037 | self.scan_unit_sysv_files() 1038 | for item in sorted(self._file_for_unit_sysv.keys()): 1039 | if not modules: 1040 | yield item 1041 | elif [ module for module in modules if fnmatch.fnmatchcase(item, module) ]: 1042 | yield item 1043 | elif [ module for module in modules if module+suffix == item ]: 1044 | yield item 1045 | def match_units(self, modules = None, suffix=".service"): # -> [ units,.. ] 1046 | """ Helper for about any command with multiple units which can 1047 | actually be glob patterns on their respective unit name. 1048 | It returns all modules if no modules pattern were given. 1049 | Also a single string as one module pattern may be given. """ 1050 | found = [] 1051 | for unit in self.match_sysd_units(modules, suffix): 1052 | if unit not in found: 1053 | found.append(unit) 1054 | for unit in self.match_sysv_units(modules, suffix): 1055 | if unit not in found: 1056 | found.append(unit) 1057 | return found 1058 | def list_service_unit_basics(self): 1059 | """ show all the basic loading state of services """ 1060 | filename = self.unit_file() # scan all 1061 | result = [] 1062 | for name, value in self._file_for_unit_sysd.items(): 1063 | result += [ (name, "SysD", value) ] 1064 | for name, value in self._file_for_unit_sysv.items(): 1065 | result += [ (name, "SysV", value) ] 1066 | return result 1067 | def list_service_units(self, *modules): # -> [ (unit,loaded+active+substate,description) ] 1068 | """ show all the service units """ 1069 | result = {} 1070 | active = {} 1071 | substate = {} 1072 | description = {} 1073 | for unit in self.match_units(modules): 1074 | result[unit] = "not-found" 1075 | active[unit] = "inactive" 1076 | substate[unit] = "dead" 1077 | description[unit] = "" 1078 | try: 1079 | conf = self.get_unit_conf(unit) 1080 | result[unit] = "loaded" 1081 | description[unit] = self.get_description_from(conf) 1082 | active[unit] = self.get_active_from(conf) 1083 | substate[unit] = self.get_substate_from(conf) 1084 | except Exception as e: 1085 | logg.warning("list-units: %s", e) 1086 | if self._unit_state: 1087 | if self._unit_state not in [ result[unit], active[unit], substate[unit] ]: 1088 | del result[unit] 1089 | return [ (unit, result[unit] + " " + active[unit] + " " + substate[unit], description[unit]) for unit in sorted(result) ] 1090 | def show_list_units(self, *modules): # -> [ (unit,loaded,description) ] 1091 | """ [PATTERN]... -- List loaded units. 1092 | If one or more PATTERNs are specified, only units matching one of 1093 | them are shown. NOTE: This is the default command.""" 1094 | hint = "To show all installed unit files use 'systemctl list-unit-files'." 1095 | result = self.list_service_units(*modules) 1096 | if self._no_legend: 1097 | return result 1098 | found = "%s loaded units listed." % len(result) 1099 | return result + [ "", found, hint ] 1100 | def list_service_unit_files(self, *modules): # -> [ (unit,enabled) ] 1101 | """ show all the service units and the enabled status""" 1102 | logg.debug("list service unit files for %s", modules) 1103 | result = {} 1104 | enabled = {} 1105 | for unit in self.match_units(modules): 1106 | result[unit] = None 1107 | enabled[unit] = "" 1108 | try: 1109 | conf = self.get_unit_conf(unit) 1110 | if self.not_user_conf(conf): 1111 | result[unit] = None 1112 | continue 1113 | result[unit] = conf 1114 | enabled[unit] = self.enabled_from(conf) 1115 | except Exception as e: 1116 | logg.warning("list-units: %s", e) 1117 | return [ (unit, enabled[unit]) for unit in sorted(result) if result[unit] ] 1118 | def each_target_file(self): 1119 | folders = self.system_folders() 1120 | if self.user_mode(): 1121 | folders = self.user_folders() 1122 | for folder in folders: 1123 | if not os.path.isdir(folder): 1124 | continue 1125 | for filename in os.listdir(folder): 1126 | if filename.endswith(".target"): 1127 | yield (filename, os.path.join(folder, filename)) 1128 | def list_target_unit_files(self, *modules): # -> [ (unit,enabled) ] 1129 | """ show all the target units and the enabled status""" 1130 | enabled = {} 1131 | targets = {} 1132 | for target, filepath in self.each_target_file(): 1133 | logg.info("target %s", filepath) 1134 | targets[target] = filepath 1135 | enabled[target] = "static" 1136 | for unit in _all_common_targets: 1137 | targets[unit] = None 1138 | enabled[unit] = "static" 1139 | if unit in _all_common_enabled: 1140 | enabled[unit] = "enabled" 1141 | if unit in _all_common_disabled: 1142 | enabled[unit] = "disabled" 1143 | return [ (unit, enabled[unit]) for unit in sorted(targets) ] 1144 | def show_list_unit_files(self, *modules): # -> [ (unit,enabled) ] 1145 | """[PATTERN]... -- List installed unit files 1146 | List installed unit files and their enablement state (as reported 1147 | by is-enabled). If one or more PATTERNs are specified, only units 1148 | whose filename (just the last component of the path) matches one of 1149 | them are shown. This command reacts to limitations of --type being 1150 | --type=service or --type=target (and --now for some basics).""" 1151 | if self._now: 1152 | result = self.list_service_unit_basics() 1153 | elif self._unit_type == "target": 1154 | result = self.list_target_unit_files() 1155 | elif self._unit_type == "service": 1156 | result = self.list_service_unit_files() 1157 | elif self._unit_type: 1158 | logg.warning("unsupported unit --type=%s", self._unit_type) 1159 | result = [] 1160 | else: 1161 | result = self.list_target_unit_files() 1162 | result += self.list_service_unit_files(*modules) 1163 | if self._no_legend: 1164 | return result 1165 | found = "%s unit files listed." % len(result) 1166 | return [ ("UNIT FILE", "STATE") ] + result + [ "", found ] 1167 | ## 1168 | ## 1169 | def get_description(self, unit, default = None): 1170 | return self.get_description_from(self.load_unit_conf(unit)) 1171 | def get_description_from(self, conf, default = None): # -> text 1172 | """ Unit.Description could be empty sometimes """ 1173 | if not conf: return default or "" 1174 | description = conf.get("Unit", "Description", default or "") 1175 | return self.expand_special(description, conf) 1176 | def read_pid_file(self, pid_file, default = None): 1177 | pid = default 1178 | if not pid_file: 1179 | return default 1180 | if not os.path.isfile(pid_file): 1181 | return default 1182 | if self.truncate_old(pid_file): 1183 | return default 1184 | try: 1185 | # some pid-files from applications contain multiple lines 1186 | for line in open(pid_file): 1187 | if line.strip(): 1188 | pid = to_int(line.strip()) 1189 | break 1190 | except Exception as e: 1191 | logg.warning("bad read of pid file '%s': %s", pid_file, e) 1192 | return pid 1193 | def wait_pid_file(self, pid_file, timeout = None): # -> pid? 1194 | """ wait some seconds for the pid file to appear and return the pid """ 1195 | timeout = int(timeout or (DefaultTimeoutStartSec/2)) 1196 | timeout = max(timeout, (MinimumTimeoutStartSec)) 1197 | dirpath = os.path.dirname(os.path.abspath(pid_file)) 1198 | for x in xrange(timeout): 1199 | if not os.path.isdir(dirpath): 1200 | time.sleep(1) # until TimeoutStartSec/2 1201 | continue 1202 | pid = self.read_pid_file(pid_file) 1203 | if not pid: 1204 | time.sleep(1) # until TimeoutStartSec/2 1205 | continue 1206 | if not pid_exists(pid): 1207 | time.sleep(1) # until TimeoutStartSec/2 1208 | continue 1209 | return pid 1210 | return None 1211 | def test_pid_file(self, unit): # -> text 1212 | """ support for the testsuite.py """ 1213 | conf = self.get_unit_conf(unit) 1214 | return self.pid_file_from(conf) or self.status_file_from(conf) 1215 | def pid_file_from(self, conf, default = ""): 1216 | """ get the specified pid file path (not a computed default) """ 1217 | pid_file = conf.get("Service", "PIDFile", default) 1218 | return self.expand_special(pid_file, conf) 1219 | def read_mainpid_from(self, conf, default): 1220 | """ MAINPID is either the PIDFile content written from the application 1221 | or it is the value in the status file written by this systemctl.py code """ 1222 | pid_file = self.pid_file_from(conf) 1223 | if pid_file: 1224 | return self.read_pid_file(pid_file, default) 1225 | status = self.read_status_from(conf) 1226 | return status.get("MainPID", default) 1227 | def clean_pid_file_from(self, conf): 1228 | pid_file = self.pid_file_from(conf) 1229 | if pid_file and os.path.isfile(pid_file): 1230 | try: 1231 | os.remove(pid_file) 1232 | except OSError as e: 1233 | logg.warning("while rm %s: %s", pid_file, e) 1234 | self.write_status_from(conf, MainPID=None) 1235 | def get_status_file(self, unit): # for testing 1236 | conf = self.get_unit_conf(unit) 1237 | return self.status_file_from(conf) 1238 | def status_file_from(self, conf, default = None): 1239 | if default is None: 1240 | default = self.default_status_file(conf) 1241 | if conf is None: return default 1242 | status_file = conf.get("Service", "StatusFile", default) 1243 | # this not a real setting, but do the expand_special anyway 1244 | return self.expand_special(status_file, conf) 1245 | def default_status_file(self, conf): # -> text 1246 | """ default file pattern where to store a status mark """ 1247 | folder = _var(self._pid_file_folder) 1248 | if self._root: 1249 | folder = os_path(self._root, folder) 1250 | name = "%s.status" % conf.name() 1251 | return os.path.join(folder, name) 1252 | def clean_status_from(self, conf): 1253 | status_file = self.status_file_from(conf) 1254 | if os.path.exists(status_file): 1255 | os.remove(status_file) 1256 | conf.status = {} 1257 | def write_status_from(self, conf, **status): # -> bool(written) 1258 | """ if a status_file is known then path is created and the 1259 | give status is written as the only content. """ 1260 | status_file = self.status_file_from(conf) 1261 | if not status_file: 1262 | logg.debug("status %s but no status_file", conf.name()) 1263 | return False 1264 | dirpath = os.path.dirname(os.path.abspath(status_file)) 1265 | if not os.path.isdir(dirpath): 1266 | os.makedirs(dirpath) 1267 | if conf.status is None: 1268 | conf.status = self.read_status_from(conf) 1269 | if True: 1270 | for key in sorted(status.keys()): 1271 | value = status[key] 1272 | if key.upper() == "AS": key = "ActiveState" 1273 | if key.upper() == "EXIT": key = "ExecMainCode" 1274 | if value is None: 1275 | try: del conf.status[key] 1276 | except KeyError: pass 1277 | else: 1278 | conf.status[key] = value 1279 | try: 1280 | with open(status_file, "w") as f: 1281 | for key in sorted(conf.status): 1282 | if key == "MainPID" and str(value) == "0": 1283 | logg.warning("ignore writing MainPID=0") 1284 | continue 1285 | value = conf.status[key] 1286 | content = "{}={}\n".format(key, str(value)) 1287 | logg.debug("writing to %s\n\t%s", status_file, content.strip()) 1288 | f.write(content) 1289 | except IOError as e: 1290 | logg.error("writing STATUS %s: %s\n\t to status file %s", status, e, status_file) 1291 | return True 1292 | def read_status_from(self, conf, defaults = None): 1293 | status_file = self.status_file_from(conf) 1294 | status = {} 1295 | if hasattr(defaults, "keys"): 1296 | for key in defaults.keys(): 1297 | status[key] = defaults[key] 1298 | elif isinstance(defaults, string_types): 1299 | status["ActiveState"] = defaults 1300 | if not status_file: 1301 | logg.debug("no status file. returning %s", status) 1302 | return status 1303 | if not os.path.isfile(status_file): 1304 | logg.debug("no status file: %s\n returning %s", status_file, status) 1305 | return status 1306 | if self.truncate_old(status_file): 1307 | logg.debug("old status file: %s\n returning %s", status_file, status) 1308 | return status 1309 | try: 1310 | logg.debug("reading %s", status_file) 1311 | for line in open(status_file): 1312 | if line.strip(): 1313 | m = re.match(r"(\w+)[:=](.*)", line) 1314 | if m: 1315 | key, value = m.group(1), m.group(2) 1316 | if key.strip(): 1317 | status[key.strip()] = value.strip() 1318 | elif line in [ "active", "inactive", "failed"]: 1319 | status["ActiveState"] = line 1320 | else: 1321 | logg.warning("ignored %s", line.strip()) 1322 | except: 1323 | logg.warning("bad read of status file '%s'", status_file) 1324 | return status 1325 | def get_status_from(self, conf, name, default = None): 1326 | if conf.status is None: 1327 | conf.status = self.read_status_from(conf) 1328 | return conf.status.get(name, default) 1329 | def set_status_from(self, conf, name, value): 1330 | if conf.status is None: 1331 | conf.status = self.read_status_from(conf) 1332 | if value is None: 1333 | try: del conf.status[name] 1334 | except KeyError: pass 1335 | else: 1336 | conf.status[name] = value 1337 | # 1338 | def get_boottime(self): 1339 | if "oldest" in COVERAGE: 1340 | return self.get_boottime_oldest() 1341 | for pid in xrange(10): 1342 | proc = "/proc/%s/status" % pid 1343 | try: 1344 | if os.path.exists(proc): 1345 | return os.path.getmtime(proc) 1346 | except Exception as e: # pragma: nocover 1347 | logg.warning("could not access %s: %s", proc, e) 1348 | return self.get_boottime_oldest() 1349 | def get_boottime_oldest(self): 1350 | # otherwise get the oldest entry in /proc 1351 | booted = time.time() 1352 | for name in os.listdir("/proc"): 1353 | proc = "/proc/%s/status" % name 1354 | try: 1355 | if os.path.exists(proc): 1356 | ctime = os.path.getmtime(proc) 1357 | if ctime < booted: 1358 | booted = ctime 1359 | except Exception as e: # pragma: nocover 1360 | logg.warning("could not access %s: %s", proc, e) 1361 | return booted 1362 | def get_filetime(self, filename): 1363 | return os.path.getmtime(filename) 1364 | def truncate_old(self, filename): 1365 | filetime = self.get_filetime(filename) 1366 | boottime = self.get_boottime() 1367 | if isinstance(filetime, float): 1368 | filetime -= EpsilonTime 1369 | if filetime >= boottime : 1370 | logg.debug(" file time: %s", datetime.datetime.fromtimestamp(filetime)) 1371 | logg.debug(" boot time: %s", datetime.datetime.fromtimestamp(boottime)) 1372 | return False # OK 1373 | logg.info("truncate old %s", filename) 1374 | logg.info(" file time: %s", datetime.datetime.fromtimestamp(filetime)) 1375 | logg.info(" boot time: %s", datetime.datetime.fromtimestamp(boottime)) 1376 | try: 1377 | shutil_truncate(filename) 1378 | except Exception as e: 1379 | logg.warning("while truncating: %s", e) 1380 | return True # truncated 1381 | def getsize(self, filename): 1382 | if not filename: 1383 | return 0 1384 | if not os.path.isfile(filename): 1385 | return 0 1386 | if self.truncate_old(filename): 1387 | return 0 1388 | try: 1389 | return os.path.getsize(filename) 1390 | except Exception as e: 1391 | logg.warning("while reading file size: %s\n of %s", e, filename) 1392 | return 0 1393 | # 1394 | def read_env_file(self, env_file): # -> generate[ (name,value) ] 1395 | """ EnvironmentFile= is being scanned """ 1396 | if env_file.startswith("-"): 1397 | env_file = env_file[1:] 1398 | if not os.path.isfile(os_path(self._root, env_file)): 1399 | return 1400 | try: 1401 | for real_line in open(os_path(self._root, env_file)): 1402 | line = real_line.strip() 1403 | if not line or line.startswith("#"): 1404 | continue 1405 | m = re.match(r"(?:export +)?([\w_]+)[=]'([^']*)'", line) 1406 | if m: 1407 | yield m.group(1), m.group(2) 1408 | continue 1409 | m = re.match(r'(?:export +)?([\w_]+)[=]"([^"]*)"', line) 1410 | if m: 1411 | yield m.group(1), m.group(2) 1412 | continue 1413 | m = re.match(r'(?:export +)?([\w_]+)[=](.*)', line) 1414 | if m: 1415 | yield m.group(1), m.group(2) 1416 | continue 1417 | except Exception as e: 1418 | logg.info("while reading %s: %s", env_file, e) 1419 | def read_env_part(self, env_part): # -> generate[ (name, value) ] 1420 | """ Environment== is being scanned """ 1421 | ## systemd Environment= spec says it is a space-seperated list of 1422 | ## assignments. In order to use a space or an equals sign in a value 1423 | ## one should enclose the whole assignment with double quotes: 1424 | ## Environment="VAR1=word word" VAR2=word3 "VAR3=$word 5 6" 1425 | ## and the $word is not expanded by other environment variables. 1426 | try: 1427 | for real_line in env_part.split("\n"): 1428 | line = real_line.strip() 1429 | for found in re.finditer(r'\s*("[\w_]+=[^"]*"|[\w_]+=\S*)', line): 1430 | part = found.group(1) 1431 | if part.startswith('"'): 1432 | part = part[1:-1] 1433 | name, value = part.split("=", 1) 1434 | yield name, value 1435 | except Exception as e: 1436 | logg.info("while reading %s: %s", env_part, e) 1437 | def show_environment(self, unit): 1438 | """ [UNIT]. -- show environment parts """ 1439 | conf = self.load_unit_conf(unit) 1440 | if conf is None: 1441 | logg.error("Unit %s could not be found.", unit) 1442 | return False 1443 | if _unit_property: 1444 | return conf.getlist("Service", _unit_property) 1445 | return self.get_env(conf) 1446 | def extra_vars(self): 1447 | return self._extra_vars # from command line 1448 | def get_env(self, conf): 1449 | env = os.environ.copy() 1450 | for env_part in conf.getlist("Service", "Environment", []): 1451 | for name, value in self.read_env_part(self.expand_special(env_part, conf)): 1452 | env[name] = value # a '$word' is not special here 1453 | for env_file in conf.getlist("Service", "EnvironmentFile", []): 1454 | for name, value in self.read_env_file(self.expand_special(env_file, conf)): 1455 | env[name] = self.expand_env(value, env) 1456 | logg.debug("extra-vars %s", self.extra_vars()) 1457 | for extra in self.extra_vars(): 1458 | if extra.startswith("@"): 1459 | for name, value in self.read_env_file(extra[1:]): 1460 | logg.info("override %s=%s", name, value) 1461 | env[name] = self.expand_env(value, env) 1462 | else: 1463 | for name, value in self.read_env_part(extra): 1464 | logg.info("override %s=%s", name, value) 1465 | env[name] = value # a '$word' is not special here 1466 | return env 1467 | def expand_env(self, cmd, env): 1468 | def get_env1(m): 1469 | if m.group(1) in env: 1470 | return env[m.group(1)] 1471 | logg.debug("can not expand $%s", m.group(1)) 1472 | return "" # empty string 1473 | def get_env2(m): 1474 | if m.group(1) in env: 1475 | return env[m.group(1)] 1476 | logg.debug("can not expand ${%s}", m.group(1)) 1477 | return "" # empty string 1478 | # 1479 | maxdepth = 20 1480 | expanded = re.sub("[$](\w+)", lambda m: get_env1(m), cmd.replace("\\\n","")) 1481 | for depth in xrange(maxdepth): 1482 | new_text = re.sub("[$][{](\w+)[}]", lambda m: get_env2(m), expanded) 1483 | if new_text == expanded: 1484 | return expanded 1485 | expanded = new_text 1486 | logg.error("shell variable expansion exceeded maxdepth %s", maxdepth) 1487 | return expanded 1488 | def expand_special(self, cmd, conf = None): 1489 | """ expand %i %t and similar special vars. They are being expanded 1490 | before any other expand_env takes place which handles shell-style 1491 | $HOME references. """ 1492 | def sh_escape(value): 1493 | return "'" + value.replace("'","\\'") + "'" 1494 | def get_confs(conf): 1495 | confs={ "%": "%" } 1496 | if not conf: 1497 | return confs 1498 | unit = parse_unit(conf.name()) 1499 | confs["N"] = unit.name 1500 | confs["n"] = sh_escape(unit.name) 1501 | confs["P"] = unit.prefix 1502 | confs["p"] = sh_escape(unit.prefix) 1503 | confs["I"] = unit.instance 1504 | confs["i"] = sh_escape(unit.instance) 1505 | confs["J"] = unit.component 1506 | confs["j"] = sh_escape(unit.component) 1507 | confs["f"] = sh_escape(conf.filename()) 1508 | VARTMP = "/var/tmp" 1509 | TMP = "/tmp" 1510 | RUN = "/run" 1511 | DAT = "/var/lib" 1512 | LOG = "/var/log" 1513 | CACHE = "/var/cache" 1514 | CONFIG = "/etc" 1515 | HOME = "/root" 1516 | USER = "root" 1517 | UID = 0 1518 | SHELL = "/bin/sh" 1519 | if self.is_user_conf(conf): 1520 | USER = os_getlogin() 1521 | HOME = get_home() 1522 | RUN = os.environ.get("XDG_RUNTIME_DIR", get_runtime_dir()) 1523 | CONFIG = os.environ.get("XDG_CONFIG_HOME", HOME + "/.config") 1524 | CACHE = os.environ.get("XDG_CACHE_HOME", HOME + "/.cache") 1525 | SHARE = os.environ.get("XDG_DATA_HOME", HOME + "/.local/share") 1526 | DAT = CONFIG 1527 | LOG = os.path.join(CONFIG, "log") 1528 | SHELL = os.environ.get("SHELL", SHELL) 1529 | VARTMP = os.environ.get("TMPDIR", os.environ.get("TEMP", os.environ.get("TMP", VARTMP))) 1530 | TMP = os.environ.get("TMPDIR", os.environ.get("TEMP", os.environ.get("TMP", TMP))) 1531 | confs["V"] = os_path(self._root, VARTMP) 1532 | confs["T"] = os_path(self._root, TMP) 1533 | confs["t"] = os_path(self._root, RUN) 1534 | confs["S"] = os_path(self._root, DAT) 1535 | confs["s"] = SHELL 1536 | confs["h"] = HOME 1537 | confs["u"] = USER 1538 | confs["C"] = os_path(self._root, CACHE) 1539 | confs["E"] = os_path(self._root, CONFIG) 1540 | return confs 1541 | def get_conf1(m): 1542 | confs = get_confs(conf) 1543 | if m.group(1) in confs: 1544 | return confs[m.group(1)] 1545 | logg.warning("can not expand %%%s", m.group(1)) 1546 | return "''" # empty escaped string 1547 | return re.sub("[%](.)", lambda m: get_conf1(m), cmd) 1548 | def exec_cmd(self, cmd, env, conf = None): 1549 | """ expand ExecCmd statements including %i and $MAINPID """ 1550 | cmd1 = cmd.replace("\\\n","") 1551 | # according to documentation the %n / %% need to be expanded where in 1552 | # most cases they are shell-escaped values. So we do it before shlex. 1553 | cmd2 = self.expand_special(cmd1, conf) 1554 | # according to documentation, when bar="one two" then the expansion 1555 | # of '$bar' is ["one","two"] and '${bar}' becomes ["one two"]. We 1556 | # tackle that by expand $bar before shlex, and the rest thereafter. 1557 | def get_env1(m): 1558 | if m.group(1) in env: 1559 | return env[m.group(1)] 1560 | logg.debug("can not expand $%s", m.group(1)) 1561 | return "" # empty string 1562 | def get_env2(m): 1563 | if m.group(1) in env: 1564 | return env[m.group(1)] 1565 | logg.debug("can not expand ${%s}", m.group(1)) 1566 | return "" # empty string 1567 | cmd3 = re.sub("[$](\w+)", lambda m: get_env1(m), cmd2) 1568 | newcmd = [] 1569 | for part in shlex.split(cmd3): 1570 | newcmd += [ re.sub("[$][{](\w+)[}]", lambda m: get_env2(m), part) ] 1571 | return newcmd 1572 | def path_journal_log(self, conf): # never None 1573 | """ /var/log/zzz.service.log or /var/log/default.unit.log """ 1574 | filename = os.path.basename(conf.filename() or "") 1575 | unitname = (conf.name() or "default")+".unit" 1576 | name = filename or unitname 1577 | log_folder = _var(self._journal_log_folder) 1578 | if self._root: 1579 | log_folder = os_path(self._root, log_folder) 1580 | log_file = name.replace(os.path.sep,".") + ".log" 1581 | if log_file.startswith("."): 1582 | log_file = "dot."+log_file 1583 | return os.path.join(log_folder, log_file) 1584 | def open_journal_log(self, conf): 1585 | log_file = self.path_journal_log(conf) 1586 | log_folder = os.path.dirname(log_file) 1587 | if not os.path.isdir(log_folder): 1588 | os.makedirs(log_folder) 1589 | return open(os.path.join(log_file), "a") 1590 | def chdir_workingdir(self, conf, check = True): 1591 | """ if specified then change the working directory """ 1592 | # the original systemd will start in '/' even if User= is given 1593 | if self._root: 1594 | os.chdir(self._root) 1595 | workingdir = conf.get("Service", "WorkingDirectory", "") 1596 | if workingdir: 1597 | ignore = False 1598 | if workingdir.startswith("-"): 1599 | workingdir = workingdir[1:] 1600 | ignore = True 1601 | into = os_path(self._root, self.expand_special(workingdir, conf)) 1602 | try: 1603 | return os.chdir(into) 1604 | except Exception as e: 1605 | if not ignore: 1606 | logg.error("chdir workingdir '%s': %s", into, e) 1607 | if check: raise 1608 | return None 1609 | def notify_socket_from(self, conf, socketfile = None): 1610 | """ creates a notify-socket for the (non-privileged) user """ 1611 | NotifySocket = collections.namedtuple("NotifySocket", ["socket", "socketfile" ]) 1612 | notify_socket_folder = _var(_notify_socket_folder) 1613 | if self._root: 1614 | notify_socket_folder = os_path(self._root, notify_socket_folder) 1615 | notify_name = "notify." + str(conf.name() or "systemctl") 1616 | notify_socket = os.path.join(notify_socket_folder, notify_name) 1617 | socketfile = socketfile or notify_socket 1618 | if len(socketfile) > 100: 1619 | logg.debug("https://unix.stackexchange.com/questions/367008/%s", 1620 | "why-is-socket-path-length-limited-to-a-hundred-chars") 1621 | logg.debug("old notify socketfile (%s) = %s", len(socketfile), socketfile) 1622 | notify_socket_folder = re.sub("^(/var)?", get_runtime_dir(), _notify_socket_folder) 1623 | notify_name = notify_name[0:min(100-len(notify_socket_folder),len(notify_name))] 1624 | socketfile = os.path.join(notify_socket_folder, notify_name) 1625 | # occurs during testsuite.py for ~user/test.tmp/root path 1626 | logg.info("new notify socketfile (%s) = %s", len(socketfile), socketfile) 1627 | try: 1628 | if not os.path.isdir(os.path.dirname(socketfile)): 1629 | os.makedirs(os.path.dirname(socketfile)) 1630 | if os.path.exists(socketfile): 1631 | os.unlink(socketfile) 1632 | except Exception as e: 1633 | logg.warning("error %s: %s", socketfile, e) 1634 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) 1635 | sock.bind(socketfile) 1636 | os.chmod(socketfile, 0o777) # the service my run under some User=setting 1637 | return NotifySocket(sock, socketfile) 1638 | def read_notify_socket(self, notify, timeout): 1639 | notify.socket.settimeout(timeout or DefaultMaximumTimeout) 1640 | result = "" 1641 | try: 1642 | result, client_address = notify.socket.recvfrom(4096) 1643 | if result: 1644 | result = result.decode("utf-8") 1645 | result_txt = result.replace("\n","|") 1646 | result_len = len(result) 1647 | logg.debug("read_notify_socket(%s):%s", result_len, result_txt) 1648 | except socket.timeout as e: 1649 | if timeout > 2: 1650 | logg.debug("socket.timeout %s", e) 1651 | return result 1652 | def wait_notify_socket(self, notify, timeout, pid = None): 1653 | if not os.path.exists(notify.socketfile): 1654 | logg.info("no $NOTIFY_SOCKET exists") 1655 | return {} 1656 | # 1657 | logg.info("wait $NOTIFY_SOCKET, timeout %s", timeout) 1658 | results = {} 1659 | seenREADY = None 1660 | for attempt in xrange(timeout+1): 1661 | if pid and not self.is_active_pid(pid): 1662 | logg.info("dead PID %s", pid) 1663 | return results 1664 | if not attempt: # first one 1665 | time.sleep(1) # until TimeoutStartSec 1666 | continue 1667 | result = self.read_notify_socket(notify, 1) # sleep max 1 second 1668 | if not result: # timeout 1669 | time.sleep(1) # until TimeoutStartSec 1670 | continue 1671 | for name, value in self.read_env_part(result): 1672 | results[name] = value 1673 | if name == "READY": 1674 | seenREADY = value 1675 | if name in ["STATUS", "ACTIVESTATE"]: 1676 | logg.debug("%s: %s", name, value) # TODO: update STATUS -> SubState 1677 | if seenREADY: 1678 | break 1679 | if not seenREADY: 1680 | logg.info(".... timeout while waiting for 'READY=1' status on $NOTIFY_SOCKET") 1681 | logg.debug("notify = %s", results) 1682 | try: 1683 | notify.socket.close() 1684 | except Exception as e: 1685 | logg.debug("socket.close %s", e) 1686 | return results 1687 | def start_modules(self, *modules): 1688 | """ [UNIT]... -- start these units 1689 | /// SPECIAL: with --now or --init it will 1690 | run the init-loop and stop the units afterwards """ 1691 | found_all = True 1692 | units = [] 1693 | for module in modules: 1694 | matched = self.match_units([ module ]) 1695 | if not matched: 1696 | logg.error("Unit %s could not be found.", unit_of(module)) 1697 | found_all = False 1698 | continue 1699 | for unit in matched: 1700 | if unit not in units: 1701 | units += [ unit ] 1702 | init = self._now or self._init 1703 | return self.start_units(units, init) and found_all 1704 | def start_units(self, units, init = None): 1705 | """ fails if any unit does not start 1706 | /// SPECIAL: may run the init-loop and 1707 | stop the named units afterwards """ 1708 | done = True 1709 | started_units = [] 1710 | for unit in self.sortedAfter(units): 1711 | started_units.append(unit) 1712 | if not self.start_unit(unit): 1713 | done = False 1714 | if init: 1715 | logg.info("init-loop start") 1716 | sig = self.init_loop_until_stop(started_units) 1717 | logg.info("init-loop %s", sig) 1718 | for unit in reversed(started_units): 1719 | self.stop_unit(unit) 1720 | return done 1721 | def start_unit(self, unit): 1722 | conf = self.load_unit_conf(unit) 1723 | if conf is None: 1724 | logg.debug("unit could not be loaded (%s)", unit) 1725 | logg.error("Unit %s could not be found.", unit) 1726 | return False 1727 | if self.not_user_conf(conf): 1728 | logg.error("Unit %s not for --user mode", unit) 1729 | return False 1730 | return self.start_unit_from(conf) 1731 | def get_TimeoutStartSec(self, conf): 1732 | timeout = conf.get("Service", "TimeoutSec", DefaultTimeoutStartSec) 1733 | timeout = conf.get("Service", "TimeoutStartSec", timeout) 1734 | return time_to_seconds(timeout, DefaultMaximumTimeout) 1735 | def start_unit_from(self, conf): 1736 | if not conf: return False 1737 | if self.syntax_check(conf) > 100: return False 1738 | with waitlock(conf): 1739 | logg.debug(" start unit %s => %s", conf.name(), conf.filename()) 1740 | return self.do_start_unit_from(conf) 1741 | def do_start_unit_from(self, conf): 1742 | timeout = self.get_TimeoutStartSec(conf) 1743 | doRemainAfterExit = conf.getbool("Service", "RemainAfterExit", "no") 1744 | runs = conf.get("Service", "Type", "simple").lower() 1745 | env = self.get_env(conf) 1746 | self.exec_check_service(conf, env, "Exec") # all... 1747 | # for StopPost on failure: 1748 | returncode = 0 1749 | service_result = "success" 1750 | if True: 1751 | if runs in [ "simple", "forking", "notify" ]: 1752 | env["MAINPID"] = str(self.read_mainpid_from(conf, "")) 1753 | for cmd in conf.getlist("Service", "ExecStartPre", []): 1754 | check, cmd = checkstatus(cmd) 1755 | newcmd = self.exec_cmd(cmd, env, conf) 1756 | logg.info(" pre-start %s", shell_cmd(newcmd)) 1757 | forkpid = os.fork() 1758 | if not forkpid: 1759 | self.execve_from(conf, newcmd, env) # pragma: nocover 1760 | run = subprocess_waitpid(forkpid) 1761 | logg.debug(" pre-start done (%s) <-%s>", 1762 | run.returncode or "OK", run.signal or "") 1763 | if runs in [ "sysv" ]: 1764 | status_file = self.status_file_from(conf) 1765 | if True: 1766 | exe = conf.filename() 1767 | cmd = "'%s' start" % exe 1768 | env["SYSTEMCTL_SKIP_REDIRECT"] = "yes" 1769 | newcmd = self.exec_cmd(cmd, env, conf) 1770 | logg.info("%s start %s", runs, shell_cmd(newcmd)) 1771 | forkpid = os.fork() 1772 | if not forkpid: # pragma: no cover 1773 | os.setsid() # detach child process from parent 1774 | self.execve_from(conf, newcmd, env) 1775 | run = subprocess_waitpid(forkpid) 1776 | self.set_status_from(conf, "ExecMainCode", run.returncode) 1777 | logg.info("%s start done (%s) <-%s>", runs, 1778 | run.returncode or "OK", run.signal or "") 1779 | active = run.returncode and "failed" or "active" 1780 | self.write_status_from(conf, AS=active ) 1781 | return True 1782 | elif runs in [ "oneshot" ]: 1783 | status_file = self.status_file_from(conf) 1784 | if self.get_status_from(conf, "ActiveState", "unknown") == "active": 1785 | logg.warning("the service was already up once") 1786 | return True 1787 | for cmd in conf.getlist("Service", "ExecStart", []): 1788 | check, cmd = checkstatus(cmd) 1789 | newcmd = self.exec_cmd(cmd, env, conf) 1790 | logg.info("%s start %s", runs, shell_cmd(newcmd)) 1791 | forkpid = os.fork() 1792 | if not forkpid: # pragma: no cover 1793 | os.setsid() # detach child process from parent 1794 | self.execve_from(conf, newcmd, env) 1795 | run = subprocess_waitpid(forkpid) 1796 | if run.returncode and check: 1797 | returncode = run.returncode 1798 | service_result = "failed" 1799 | logg.error("%s start %s (%s) <-%s>", runs, service_result, 1800 | run.returncode or "OK", run.signal or "") 1801 | break 1802 | logg.info("%s start done (%s) <-%s>", runs, 1803 | run.returncode or "OK", run.signal or "") 1804 | if True: 1805 | self.set_status_from(conf, "ExecMainCode", returncode) 1806 | active = returncode and "failed" or "active" 1807 | self.write_status_from(conf, AS=active) 1808 | elif runs in [ "simple" ]: 1809 | status_file = self.status_file_from(conf) 1810 | pid = self.read_mainpid_from(conf, "") 1811 | if self.is_active_pid(pid): 1812 | logg.warning("the service is already running on PID %s", pid) 1813 | return True 1814 | if doRemainAfterExit: 1815 | logg.debug("%s RemainAfterExit -> AS=active", runs) 1816 | self.write_status_from(conf, AS="active") 1817 | cmdlist = conf.getlist("Service", "ExecStart", []) 1818 | for idx, cmd in enumerate(cmdlist): 1819 | logg.debug("ExecStart[%s]: %s", idx, cmd) 1820 | for cmd in cmdlist: 1821 | pid = self.read_mainpid_from(conf, "") 1822 | env["MAINPID"] = str(pid) 1823 | newcmd = self.exec_cmd(cmd, env, conf) 1824 | logg.info("%s start %s", runs, shell_cmd(newcmd)) 1825 | forkpid = os.fork() 1826 | if not forkpid: # pragma: no cover 1827 | os.setsid() # detach child process from parent 1828 | self.execve_from(conf, newcmd, env) 1829 | self.write_status_from(conf, MainPID=forkpid) 1830 | logg.info("%s started PID %s", runs, forkpid) 1831 | env["MAINPID"] = str(forkpid) 1832 | time.sleep(MinimumYield) 1833 | run = subprocess_testpid(forkpid) 1834 | if run.returncode is not None: 1835 | logg.info("%s stopped PID %s (%s) <-%s>", runs, run.pid, 1836 | run.returncode or "OK", run.signal or "") 1837 | if doRemainAfterExit: 1838 | self.set_status_from(conf, "ExecMainCode", run.returncode) 1839 | active = run.returncode and "failed" or "active" 1840 | self.write_status_from(conf, AS=active) 1841 | if run.returncode: 1842 | service_result = "failed" 1843 | break 1844 | elif runs in [ "notify" ]: 1845 | # "notify" is the same as "simple" but we create a $NOTIFY_SOCKET 1846 | # and wait for startup completion by checking the socket messages 1847 | pid = self.read_mainpid_from(conf, "") 1848 | if self.is_active_pid(pid): 1849 | logg.error("the service is already running on PID %s", pid) 1850 | return False 1851 | notify = self.notify_socket_from(conf) 1852 | if notify: 1853 | env["NOTIFY_SOCKET"] = notify.socketfile 1854 | logg.debug("use NOTIFY_SOCKET=%s", notify.socketfile) 1855 | if doRemainAfterExit: 1856 | logg.debug("%s RemainAfterExit -> AS=active", runs) 1857 | self.write_status_from(conf, AS="active") 1858 | cmdlist = conf.getlist("Service", "ExecStart", []) 1859 | for idx, cmd in enumerate(cmdlist): 1860 | logg.debug("ExecStart[%s]: %s", idx, cmd) 1861 | mainpid = None 1862 | for cmd in cmdlist: 1863 | mainpid = self.read_mainpid_from(conf, "") 1864 | env["MAINPID"] = str(mainpid) 1865 | newcmd = self.exec_cmd(cmd, env, conf) 1866 | logg.info("%s start %s", runs, shell_cmd(newcmd)) 1867 | forkpid = os.fork() 1868 | if not forkpid: # pragma: no cover 1869 | os.setsid() # detach child process from parent 1870 | self.execve_from(conf, newcmd, env) 1871 | # via NOTIFY # self.write_status_from(conf, MainPID=forkpid) 1872 | logg.info("%s started PID %s", runs, forkpid) 1873 | mainpid = forkpid 1874 | self.write_status_from(conf, MainPID=mainpid) 1875 | env["MAINPID"] = str(mainpid) 1876 | time.sleep(MinimumYield) 1877 | run = subprocess_testpid(forkpid) 1878 | if run.returncode is not None: 1879 | logg.info("%s stopped PID %s (%s) <-%s>", runs, run.pid, 1880 | run.returncode or "OK", run.signal or "") 1881 | if doRemainAfterExit: 1882 | self.set_status_from(conf, "ExecMainCode", run.returncode or 0) 1883 | active = run.returncode and "failed" or "active" 1884 | self.write_status_from(conf, AS=active) 1885 | if run.returncode: 1886 | service_result = "failed" 1887 | break 1888 | if service_result in [ "success" ] and mainpid: 1889 | logg.debug("okay, wating on socket for %ss", timeout) 1890 | results = self.wait_notify_socket(notify, timeout, mainpid) 1891 | if "MAINPID" in results: 1892 | new_pid = results["MAINPID"] 1893 | if new_pid and to_int(new_pid) != mainpid: 1894 | logg.info("NEW PID %s from sd_notify (was PID %s)", new_pid, mainpid) 1895 | self.write_status_from(conf, MainPID=new_pid) 1896 | mainpid = new_pid 1897 | logg.info("%s start done %s", runs, mainpid) 1898 | pid = self.read_mainpid_from(conf, "") 1899 | if pid: 1900 | env["MAINPID"] = str(pid) 1901 | else: 1902 | service_result = "timeout" # "could not start service" 1903 | elif runs in [ "forking" ]: 1904 | pid_file = self.pid_file_from(conf) 1905 | for cmd in conf.getlist("Service", "ExecStart", []): 1906 | check, cmd = checkstatus(cmd) 1907 | newcmd = self.exec_cmd(cmd, env, conf) 1908 | if not newcmd: continue 1909 | logg.info("%s start %s", runs, shell_cmd(newcmd)) 1910 | forkpid = os.fork() 1911 | if not forkpid: # pragma: no cover 1912 | os.setsid() # detach child process from parent 1913 | self.execve_from(conf, newcmd, env) 1914 | logg.info("%s started PID %s", runs, forkpid) 1915 | run = subprocess_waitpid(forkpid) 1916 | if run.returncode and check: 1917 | returncode = run.returncode 1918 | service_result = "failed" 1919 | logg.info("%s stopped PID %s (%s) <-%s>", runs, run.pid, 1920 | run.returncode or "OK", run.signal or "") 1921 | if pid_file and service_result in [ "success" ]: 1922 | pid = self.wait_pid_file(pid_file) # application PIDFile 1923 | logg.info("%s start done PID %s [%s]", runs, pid, pid_file) 1924 | if pid: 1925 | env["MAINPID"] = str(pid) 1926 | if not pid_file: 1927 | time.sleep(MinimumTimeoutStartSec) 1928 | logg.warning("No PIDFile for forking %s", conf.filename()) 1929 | status_file = self.status_file_from(conf) 1930 | self.set_status_from(conf, "ExecMainCode", returncode) 1931 | active = returncode and "failed" or "active" 1932 | self.write_status_from(conf, AS=active) 1933 | else: 1934 | logg.error("unsupported run type '%s'", runs) 1935 | return False 1936 | # POST sequence 1937 | active = self.is_active_from(conf) 1938 | if not active: 1939 | logg.warning("%s start not active", runs) 1940 | # according to the systemd documentation, a failed start-sequence 1941 | # should execute the ExecStopPost sequence allowing some cleanup. 1942 | env["SERVICE_RESULT"] = service_result 1943 | for cmd in conf.getlist("Service", "ExecStopPost", []): 1944 | check, cmd = checkstatus(cmd) 1945 | newcmd = self.exec_cmd(cmd, env, conf) 1946 | logg.info("post-fail %s", shell_cmd(newcmd)) 1947 | forkpid = os.fork() 1948 | if not forkpid: 1949 | self.execve_from(conf, newcmd, env) # pragma: nocover 1950 | run = subprocess_waitpid(forkpid) 1951 | logg.debug("post-fail done (%s) <-%s>", 1952 | run.returncode or "OK", run.signal or "") 1953 | return False 1954 | else: 1955 | for cmd in conf.getlist("Service", "ExecStartPost", []): 1956 | check, cmd = checkstatus(cmd) 1957 | newcmd = self.exec_cmd(cmd, env, conf) 1958 | logg.info("post-start %s", shell_cmd(newcmd)) 1959 | forkpid = os.fork() 1960 | if not forkpid: 1961 | self.execve_from(conf, newcmd, env) # pragma: nocover 1962 | run = subprocess_waitpid(forkpid) 1963 | logg.debug("post-start done (%s) <-%s>", 1964 | run.returncode or "OK", run.signal or "") 1965 | return True 1966 | def extend_exec_env(self, env): 1967 | env = env.copy() 1968 | # implant DefaultPath into $PATH 1969 | path = env.get("PATH", DefaultPath) 1970 | parts = path.split(os.pathsep) 1971 | for part in DefaultPath.split(os.pathsep): 1972 | if part and part not in parts: 1973 | parts.append(part) 1974 | env["PATH"] = str(os.pathsep).join(parts) 1975 | # reset locale to system default 1976 | for name in ResetLocale: 1977 | if name in env: 1978 | del env[name] 1979 | locale = {} 1980 | for var, val in self.read_env_file("/etc/locale.conf"): 1981 | locale[var] = val 1982 | env[var] = val 1983 | if "LANG" not in locale: 1984 | env["LANG"] = locale.get("LANGUAGE", locale.get("LC_CTYPE", "C")) 1985 | return env 1986 | def execve_from(self, conf, cmd, env): 1987 | """ this code is commonly run in a child process // returns exit-code""" 1988 | runs = conf.get("Service", "Type", "simple").lower() 1989 | logg.debug("%s process for %s", runs, conf.filename()) 1990 | inp = open("/dev/zero") 1991 | out = self.open_journal_log(conf) 1992 | os.dup2(inp.fileno(), sys.stdin.fileno()) 1993 | os.dup2(out.fileno(), sys.stdout.fileno()) 1994 | os.dup2(out.fileno(), sys.stderr.fileno()) 1995 | runuser = self.expand_special(conf.get("Service", "User", ""), conf) 1996 | rungroup = self.expand_special(conf.get("Service", "Group", ""), conf) 1997 | envs = shutil_setuid(runuser, rungroup) 1998 | self.chdir_workingdir(conf, check = False) # some dirs need setuid before 1999 | env = self.extend_exec_env(env) 2000 | env.update(envs) # set $HOME to ~$USER 2001 | try: 2002 | if "spawn" in COVERAGE: 2003 | os.spawnvpe(os.P_WAIT, cmd[0], cmd, env) 2004 | sys.exit(0) 2005 | else: # pragma: nocover 2006 | os.execve(cmd[0], cmd, env) 2007 | except Exception as e: 2008 | logg.error("(%s): %s", shell_cmd(cmd), e) 2009 | sys.exit(1) 2010 | def test_start_unit(self, unit): 2011 | """ helper function to test the code that is normally forked off """ 2012 | conf = self.load_unit_conf(unit) 2013 | env = self.get_env(conf) 2014 | for cmd in conf.getlist("Service", "ExecStart", []): 2015 | newcmd = self.exec_cmd(cmd, env, conf) 2016 | return self.execve_from(conf, newcmd, env) 2017 | return None 2018 | def stop_modules(self, *modules): 2019 | """ [UNIT]... -- stop these units """ 2020 | found_all = True 2021 | units = [] 2022 | for module in modules: 2023 | matched = self.match_units([ module ]) 2024 | if not matched: 2025 | logg.error("Unit %s could not be found.", unit_of(module)) 2026 | found_all = False 2027 | continue 2028 | for unit in matched: 2029 | if unit not in units: 2030 | units += [ unit ] 2031 | return self.stop_units(units) and found_all 2032 | def stop_units(self, units): 2033 | """ fails if any unit fails to stop """ 2034 | done = True 2035 | for unit in self.sortedBefore(units): 2036 | if not self.stop_unit(unit): 2037 | done = False 2038 | return done 2039 | def stop_unit(self, unit): 2040 | conf = self.load_unit_conf(unit) 2041 | if conf is None: 2042 | logg.error("Unit %s could not be found.", unit) 2043 | return False 2044 | if self.not_user_conf(conf): 2045 | logg.error("Unit %s not for --user mode", unit) 2046 | return False 2047 | return self.stop_unit_from(conf) 2048 | 2049 | def get_TimeoutStopSec(self, conf): 2050 | timeout = conf.get("Service", "TimeoutSec", DefaultTimeoutStartSec) 2051 | timeout = conf.get("Service", "TimeoutStopSec", timeout) 2052 | return time_to_seconds(timeout, DefaultMaximumTimeout) 2053 | def stop_unit_from(self, conf): 2054 | if not conf: return False 2055 | if self.syntax_check(conf) > 100: return False 2056 | with waitlock(conf): 2057 | logg.info(" stop unit %s => %s", conf.name(), conf.filename()) 2058 | return self.do_stop_unit_from(conf) 2059 | def do_stop_unit_from(self, conf): 2060 | timeout = self.get_TimeoutStopSec(conf) 2061 | runs = conf.get("Service", "Type", "simple").lower() 2062 | env = self.get_env(conf) 2063 | self.exec_check_service(conf, env, "ExecStop") 2064 | returncode = 0 2065 | service_result = "success" 2066 | if runs in [ "sysv" ]: 2067 | status_file = self.status_file_from(conf) 2068 | if True: 2069 | exe = conf.filename() 2070 | cmd = "'%s' stop" % exe 2071 | env["SYSTEMCTL_SKIP_REDIRECT"] = "yes" 2072 | newcmd = self.exec_cmd(cmd, env, conf) 2073 | logg.info("%s stop %s", runs, shell_cmd(newcmd)) 2074 | forkpid = os.fork() 2075 | if not forkpid: 2076 | self.execve_from(conf, newcmd, env) # pragma: nocover 2077 | run = subprocess_waitpid(forkpid) 2078 | if run.returncode: 2079 | self.set_status_from(conf, "ExecStopCode", run.returncode) 2080 | self.write_status_from(conf, AS="failed") 2081 | else: 2082 | self.clean_status_from(conf) # "inactive" 2083 | return True 2084 | elif runs in [ "oneshot" ]: 2085 | status_file = self.status_file_from(conf) 2086 | if self.get_status_from(conf, "ActiveState", "unknown") == "inactive": 2087 | logg.warning("the service is already down once") 2088 | return True 2089 | for cmd in conf.getlist("Service", "ExecStop", []): 2090 | check, cmd = checkstatus(cmd) 2091 | logg.debug("{env} %s", env) 2092 | newcmd = self.exec_cmd(cmd, env, conf) 2093 | logg.info("%s stop %s", runs, shell_cmd(newcmd)) 2094 | forkpid = os.fork() 2095 | if not forkpid: 2096 | self.execve_from(conf, newcmd, env) # pragma: nocover 2097 | run = subprocess_waitpid(forkpid) 2098 | if run.returncode and check: 2099 | returncode = run.returncode 2100 | service_result = "failed" 2101 | break 2102 | if True: 2103 | if returncode: 2104 | self.set_status_from(conf, "ExecStopCode", returncode) 2105 | self.write_status_from(conf, AS="failed") 2106 | else: 2107 | self.clean_status_from(conf) # "inactive" 2108 | ### fallback Stop => Kill for ["simple","notify","forking"] 2109 | elif not conf.getlist("Service", "ExecStop", []): 2110 | logg.info("no ExecStop => systemctl kill") 2111 | if True: 2112 | self.do_kill_unit_from(conf) 2113 | self.clean_pid_file_from(conf) 2114 | self.clean_status_from(conf) # "inactive" 2115 | elif runs in [ "simple", "notify" ]: 2116 | status_file = self.status_file_from(conf) 2117 | size = os.path.exists(status_file) and os.path.getsize(status_file) 2118 | logg.info("STATUS %s %s", status_file, size) 2119 | pid = 0 2120 | for cmd in conf.getlist("Service", "ExecStop", []): 2121 | check, cmd = checkstatus(cmd) 2122 | env["MAINPID"] = str(self.read_mainpid_from(conf, "")) 2123 | newcmd = self.exec_cmd(cmd, env, conf) 2124 | logg.info("%s stop %s", runs, shell_cmd(newcmd)) 2125 | forkpid = os.fork() 2126 | if not forkpid: 2127 | self.execve_from(conf, newcmd, env) # pragma: nocover 2128 | run = subprocess_waitpid(forkpid) 2129 | run = must_have_failed(run, newcmd) # TODO: a workaround 2130 | # self.write_status_from(conf, MainPID=run.pid) # no ExecStop 2131 | if run.returncode and check: 2132 | returncode = run.returncode 2133 | service_result = "failed" 2134 | break 2135 | pid = env.get("MAINPID",0) 2136 | if pid: 2137 | if self.wait_vanished_pid(pid, timeout): 2138 | self.clean_pid_file_from(conf) 2139 | self.clean_status_from(conf) # "inactive" 2140 | else: 2141 | logg.info("%s sleep as no PID was found on Stop", runs) 2142 | time.sleep(MinimumTimeoutStopSec) 2143 | pid = self.read_mainpid_from(conf, "") 2144 | if not pid or not pid_exists(pid) or pid_zombie(pid): 2145 | self.clean_pid_file_from(conf) 2146 | self.clean_status_from(conf) # "inactive" 2147 | elif runs in [ "forking" ]: 2148 | status_file = self.status_file_from(conf) 2149 | pid_file = self.pid_file_from(conf) 2150 | for cmd in conf.getlist("Service", "ExecStop", []): 2151 | active = self.is_active_from(conf) 2152 | if pid_file: 2153 | new_pid = self.read_mainpid_from(conf, "") 2154 | if new_pid: 2155 | env["MAINPID"] = str(new_pid) 2156 | check, cmd = checkstatus(cmd) 2157 | logg.debug("{env} %s", env) 2158 | newcmd = self.exec_cmd(cmd, env, conf) 2159 | logg.info("fork stop %s", shell_cmd(newcmd)) 2160 | forkpid = os.fork() 2161 | if not forkpid: 2162 | self.execve_from(conf, newcmd, env) # pragma: nocover 2163 | run = subprocess_waitpid(forkpid) 2164 | if run.returncode and check: 2165 | returncode = run.returncode 2166 | service_result = "failed" 2167 | break 2168 | pid = env.get("MAINPID",0) 2169 | if pid: 2170 | if self.wait_vanished_pid(pid, timeout): 2171 | self.clean_pid_file_from(conf) 2172 | else: 2173 | logg.info("%s sleep as no PID was found on Stop", runs) 2174 | time.sleep(MinimumTimeoutStopSec) 2175 | pid = self.read_mainpid_from(conf, "") 2176 | if not pid or not pid_exists(pid) or pid_zombie(pid): 2177 | self.clean_pid_file_from(conf) 2178 | if returncode: 2179 | if os.path.isfile(status_file): 2180 | self.set_status_from(conf, "ExecStopCode", returncode) 2181 | self.write_status_from(conf, AS="failed") 2182 | else: 2183 | self.clean_status_from(conf) # "inactive" 2184 | else: 2185 | logg.error("unsupported run type '%s'", runs) 2186 | return False 2187 | # POST sequence 2188 | active = self.is_active_from(conf) 2189 | if not active: 2190 | env["SERVICE_RESULT"] = service_result 2191 | for cmd in conf.getlist("Service", "ExecStopPost", []): 2192 | check, cmd = checkstatus(cmd) 2193 | newcmd = self.exec_cmd(cmd, env, conf) 2194 | logg.info("post-stop %s", shell_cmd(newcmd)) 2195 | forkpid = os.fork() 2196 | if not forkpid: 2197 | self.execve_from(conf, newcmd, env) # pragma: nocover 2198 | run = subprocess_waitpid(forkpid) 2199 | logg.debug("post-stop done (%s) <-%s>", 2200 | run.returncode or "OK", run.signal or "") 2201 | return service_result == "success" 2202 | def wait_vanished_pid(self, pid, timeout): 2203 | if not pid: 2204 | return True 2205 | logg.info("wait for PID %s to vanish (%ss)", pid, timeout) 2206 | for x in xrange(int(timeout)): 2207 | if not self.is_active_pid(pid): 2208 | logg.info("wait for PID %s is done (%s.)", pid, x) 2209 | return True 2210 | time.sleep(1) # until TimeoutStopSec 2211 | logg.info("wait for PID %s failed (%s.)", pid, x) 2212 | return False 2213 | def reload_modules(self, *modules): 2214 | """ [UNIT]... -- reload these units """ 2215 | found_all = True 2216 | units = [] 2217 | for module in modules: 2218 | matched = self.match_units([ module ]) 2219 | if not matched: 2220 | logg.error("Unit %s could not be found.", unit_of(module)) 2221 | found_all = False 2222 | continue 2223 | for unit in matched: 2224 | if unit not in units: 2225 | units += [ unit ] 2226 | return self.reload_units(units) and found_all 2227 | def reload_units(self, units): 2228 | """ fails if any unit fails to reload """ 2229 | done = True 2230 | for unit in self.sortedAfter(units): 2231 | if not self.reload_unit(unit): 2232 | done = False 2233 | return done 2234 | def reload_unit(self, unit): 2235 | conf = self.load_unit_conf(unit) 2236 | if conf is None: 2237 | logg.error("Unit %s could not be found.", unit) 2238 | return False 2239 | if self.not_user_conf(conf): 2240 | logg.error("Unit %s not for --user mode", unit) 2241 | return False 2242 | return self.reload_unit_from(conf) 2243 | def reload_unit_from(self, conf): 2244 | if not conf: return False 2245 | if self.syntax_check(conf) > 100: return False 2246 | with waitlock(conf): 2247 | logg.info(" reload unit %s => %s", conf.name(), conf.filename()) 2248 | return self.do_reload_unit_from(conf) 2249 | def do_reload_unit_from(self, conf): 2250 | runs = conf.get("Service", "Type", "simple").lower() 2251 | env = self.get_env(conf) 2252 | self.exec_check_service(conf, env, "ExecReload") 2253 | if runs in [ "sysv" ]: 2254 | status_file = self.status_file_from(conf) 2255 | if True: 2256 | exe = conf.filename() 2257 | cmd = "'%s' reload" % exe 2258 | env["SYSTEMCTL_SKIP_REDIRECT"] = "yes" 2259 | newcmd = self.exec_cmd(cmd, env, conf) 2260 | logg.info("%s reload %s", runs, shell_cmd(newcmd)) 2261 | forkpid = os.fork() 2262 | if not forkpid: 2263 | self.execve_from(conf, newcmd, env) # pragma: nocover 2264 | run = subprocess_waitpid(forkpid) 2265 | self.set_status_from(conf, "ExecReloadCode", run.returncode) 2266 | if run.returncode: 2267 | self.write_status_from(conf, AS="failed") 2268 | return False 2269 | else: 2270 | self.write_status_from(conf, AS="active") 2271 | return True 2272 | elif runs in [ "simple", "notify", "forking" ]: 2273 | if not self.is_active_from(conf): 2274 | logg.info("no reload on inactive service %s", conf.name()) 2275 | return True 2276 | for cmd in conf.getlist("Service", "ExecReload", []): 2277 | env["MAINPID"] = str(self.read_mainpid_from(conf, "")) 2278 | check, cmd = checkstatus(cmd) 2279 | newcmd = self.exec_cmd(cmd, env, conf) 2280 | logg.info("%s reload %s", runs, shell_cmd(newcmd)) 2281 | forkpid = os.fork() 2282 | if not forkpid: 2283 | self.execve_from(conf, newcmd, env) # pragma: nocover 2284 | run = subprocess_waitpid(forkpid) 2285 | if check and run.returncode: 2286 | logg.error("Job for %s failed because the control process exited with error code. (%s)", 2287 | conf.name(), run.returncode) 2288 | return False 2289 | time.sleep(MinimumYield) 2290 | return True 2291 | elif runs in [ "oneshot" ]: 2292 | logg.debug("ignored run type '%s' for reload", runs) 2293 | return True 2294 | else: 2295 | logg.error("unsupported run type '%s'", runs) 2296 | return False 2297 | def restart_modules(self, *modules): 2298 | """ [UNIT]... -- restart these units """ 2299 | found_all = True 2300 | units = [] 2301 | for module in modules: 2302 | matched = self.match_units([ module ]) 2303 | if not matched: 2304 | logg.error("Unit %s could not be found.", unit_of(module)) 2305 | found_all = False 2306 | continue 2307 | for unit in matched: 2308 | if unit not in units: 2309 | units += [ unit ] 2310 | return self.restart_units(units) and found_all 2311 | def restart_units(self, units): 2312 | """ fails if any unit fails to restart """ 2313 | done = True 2314 | for unit in self.sortedAfter(units): 2315 | if not self.restart_unit(unit): 2316 | done = False 2317 | return done 2318 | def restart_unit(self, unit): 2319 | conf = self.load_unit_conf(unit) 2320 | if conf is None: 2321 | logg.error("Unit %s could not be found.", unit) 2322 | return False 2323 | if self.not_user_conf(conf): 2324 | logg.error("Unit %s not for --user mode", unit) 2325 | return False 2326 | return self.restart_unit_from(conf) 2327 | def restart_unit_from(self, conf): 2328 | if not conf: return False 2329 | if self.syntax_check(conf) > 100: return False 2330 | with waitlock(conf): 2331 | logg.info(" restart unit %s => %s", conf.name(), conf.filename()) 2332 | if not self.is_active_from(conf): 2333 | return self.do_start_unit_from(conf) 2334 | else: 2335 | return self.do_restart_unit_from(conf) 2336 | def do_restart_unit_from(self, conf): 2337 | logg.info("(restart) => stop/start") 2338 | self.do_stop_unit_from(conf) 2339 | return self.do_start_unit_from(conf) 2340 | def try_restart_modules(self, *modules): 2341 | """ [UNIT]... -- try-restart these units """ 2342 | found_all = True 2343 | units = [] 2344 | for module in modules: 2345 | matched = self.match_units([ module ]) 2346 | if not matched: 2347 | logg.error("Unit %s could not be found.", unit_of(module)) 2348 | found_all = False 2349 | continue 2350 | for unit in matched: 2351 | if unit not in units: 2352 | units += [ unit ] 2353 | return self.try_restart_units(units) and found_all 2354 | def try_restart_units(self, units): 2355 | """ fails if any module fails to try-restart """ 2356 | done = True 2357 | for unit in self.sortedAfter(units): 2358 | if not self.try_restart_unit(unit): 2359 | done = False 2360 | return done 2361 | def try_restart_unit(self, unit): 2362 | """ only do 'restart' if 'active' """ 2363 | conf = self.load_unit_conf(unit) 2364 | if conf is None: 2365 | logg.error("Unit %s could not be found.", unit) 2366 | return False 2367 | if self.not_user_conf(conf): 2368 | logg.error("Unit %s not for --user mode", unit) 2369 | return False 2370 | with waitlock(conf): 2371 | logg.info(" try-restart unit %s => %s", conf.name(), conf.filename()) 2372 | if self.is_active_from(conf): 2373 | return self.do_restart_unit_from(conf) 2374 | return True 2375 | def reload_or_restart_modules(self, *modules): 2376 | """ [UNIT]... -- reload-or-restart these units """ 2377 | found_all = True 2378 | units = [] 2379 | for module in modules: 2380 | matched = self.match_units([ module ]) 2381 | if not matched: 2382 | logg.error("Unit %s could not be found.", unit_of(module)) 2383 | found_all = False 2384 | continue 2385 | for unit in matched: 2386 | if unit not in units: 2387 | units += [ unit ] 2388 | return self.reload_or_restart_units(units) and found_all 2389 | def reload_or_restart_units(self, units): 2390 | """ fails if any unit does not reload-or-restart """ 2391 | done = True 2392 | for unit in self.sortedAfter(units): 2393 | if not self.reload_or_restart_unit(unit): 2394 | done = False 2395 | return done 2396 | def reload_or_restart_unit(self, unit): 2397 | """ do 'reload' if specified, otherwise do 'restart' """ 2398 | conf = self.load_unit_conf(unit) 2399 | if conf is None: 2400 | logg.error("Unit %s could not be found.", unit) 2401 | return False 2402 | if self.not_user_conf(conf): 2403 | logg.error("Unit %s not for --user mode", unit) 2404 | return False 2405 | return self.reload_or_restart_unit_from(conf) 2406 | def reload_or_restart_unit_from(self, conf): 2407 | """ do 'reload' if specified, otherwise do 'restart' """ 2408 | if not conf: return False 2409 | with waitlock(conf): 2410 | logg.info(" reload-or-restart unit %s => %s", conf.name(), conf.filename()) 2411 | return self.do_reload_or_restart_unit_from(conf) 2412 | def do_reload_or_restart_unit_from(self, conf): 2413 | if not self.is_active_from(conf): 2414 | # try: self.stop_unit_from(conf) 2415 | # except Exception as e: pass 2416 | return self.do_start_unit_from(conf) 2417 | elif conf.getlist("Service", "ExecReload", []): 2418 | logg.info("found service to have ExecReload -> 'reload'") 2419 | return self.do_reload_unit_from(conf) 2420 | else: 2421 | logg.info("found service without ExecReload -> 'restart'") 2422 | return self.do_restart_unit_from(conf) 2423 | def reload_or_try_restart_modules(self, *modules): 2424 | """ [UNIT]... -- reload-or-try-restart these units """ 2425 | found_all = True 2426 | units = [] 2427 | for module in modules: 2428 | matched = self.match_units([ module ]) 2429 | if not matched: 2430 | logg.error("Unit %s could not be found.", unit_of(module)) 2431 | found_all = False 2432 | continue 2433 | for unit in matched: 2434 | if unit not in units: 2435 | units += [ unit ] 2436 | return self.reload_or_try_restart_units(units) and found_all 2437 | def reload_or_try_restart_units(self, units): 2438 | """ fails if any unit fails to reload-or-try-restart """ 2439 | done = True 2440 | for unit in self.sortedAfter(units): 2441 | if not self.reload_or_try_restart_unit(unit): 2442 | done = False 2443 | return done 2444 | def reload_or_try_restart_unit(self, unit): 2445 | conf = self.load_unit_conf(unit) 2446 | if conf is None: 2447 | logg.error("Unit %s could not be found.", unit) 2448 | return False 2449 | if self.not_user_conf(conf): 2450 | logg.error("Unit %s not for --user mode", unit) 2451 | return False 2452 | return self.reload_or_try_restart_unit_from(conf) 2453 | def reload_or_try_restart_unit_from(self, conf): 2454 | with waitlock(conf): 2455 | logg.info(" reload-or-try-restart unit %s => %s", conf.name(), conf.filename()) 2456 | return self.do_reload_or_try_restart_unit_from(conf) 2457 | def do_reload_or_try_restart_unit_from(self, conf): 2458 | if conf.getlist("Service", "ExecReload", []): 2459 | return self.do_reload_unit_from(conf) 2460 | elif not self.is_active_from(conf): 2461 | return True 2462 | else: 2463 | return self.do_restart_unit_from(conf) 2464 | def kill_modules(self, *modules): 2465 | """ [UNIT]... -- kill these units """ 2466 | found_all = True 2467 | units = [] 2468 | for module in modules: 2469 | matched = self.match_units([ module ]) 2470 | if not matched: 2471 | logg.error("Unit %s could not be found.", unit_of(module)) 2472 | found_all = False 2473 | continue 2474 | for unit in matched: 2475 | if unit not in units: 2476 | units += [ unit ] 2477 | return self.kill_units(units) and found_all 2478 | def kill_units(self, units): 2479 | """ fails if any unit could not be killed """ 2480 | done = True 2481 | for unit in self.sortedBefore(units): 2482 | if not self.kill_unit(unit): 2483 | done = False 2484 | return done 2485 | def kill_unit(self, unit): 2486 | conf = self.load_unit_conf(unit) 2487 | if conf is None: 2488 | logg.error("Unit %s could not be found.", unit) 2489 | return False 2490 | if self.not_user_conf(conf): 2491 | logg.error("Unit %s not for --user mode", unit) 2492 | return False 2493 | return self.kill_unit_from(conf) 2494 | def kill_unit_from(self, conf): 2495 | if not conf: return False 2496 | with waitlock(conf): 2497 | logg.info(" kill unit %s => %s", conf.name(), conf.filename()) 2498 | return self.do_kill_unit_from(conf) 2499 | def do_kill_unit_from(self, conf): 2500 | started = time.time() 2501 | doSendSIGKILL = conf.getbool("Service", "SendSIGKILL", "yes") 2502 | doSendSIGHUP = conf.getbool("Service", "SendSIGHUP", "no") 2503 | useKillMode = conf.get("Service", "KillMode", "control-group") 2504 | useKillSignal = conf.get("Service", "KillSignal", "SIGTERM") 2505 | kill_signal = getattr(signal, useKillSignal) 2506 | timeout = self.get_TimeoutStopSec(conf) 2507 | status_file = self.status_file_from(conf) 2508 | size = os.path.exists(status_file) and os.path.getsize(status_file) 2509 | logg.info("STATUS %s %s", status_file, size) 2510 | mainpid = to_int(self.read_mainpid_from(conf, "")) 2511 | self.clean_status_from(conf) # clear RemainAfterExit and TimeoutStartSec 2512 | if not mainpid: 2513 | if useKillMode in ["control-group"]: 2514 | logg.warning("no main PID [%s]", conf.filename()) 2515 | logg.warning("and there is no control-group here") 2516 | else: 2517 | logg.info("no main PID [%s]", conf.filename()) 2518 | return False 2519 | if not pid_exists(mainpid) or pid_zombie(mainpid): 2520 | logg.debug("ignoring children when mainpid is already dead") 2521 | # because we list child processes, not processes in control-group 2522 | return True 2523 | pidlist = self.pidlist_of(mainpid) # here 2524 | if pid_exists(mainpid): 2525 | logg.info("stop kill PID %s", mainpid) 2526 | self._kill_pid(mainpid, kill_signal) 2527 | if useKillMode in ["control-group"]: 2528 | if len(pidlist) > 1: 2529 | logg.info("stop control-group PIDs %s", pidlist) 2530 | for pid in pidlist: 2531 | if pid != mainpid: 2532 | self._kill_pid(pid, kill_signal) 2533 | if doSendSIGHUP: 2534 | logg.info("stop SendSIGHUP to PIDs %s", pidlist) 2535 | for pid in pidlist: 2536 | self._kill_pid(pid, signal.SIGHUP) 2537 | # wait for the processes to have exited 2538 | while True: 2539 | dead = True 2540 | for pid in pidlist: 2541 | if pid_exists(pid) and not pid_zombie(pid): 2542 | dead = False 2543 | break 2544 | if dead: 2545 | break 2546 | if time.time() > started + timeout: 2547 | logg.info("service PIDs not stopped after %s", timeout) 2548 | break 2549 | time.sleep(1) # until TimeoutStopSec 2550 | if dead or not doSendSIGKILL: 2551 | logg.info("done kill PID %s %s", mainpid, dead and "OK") 2552 | return dead 2553 | if useKillMode in [ "control-group", "mixed" ]: 2554 | logg.info("hard kill PIDs %s", pidlist) 2555 | for pid in pidlist: 2556 | if pid != mainpid: 2557 | self._kill_pid(pid, signal.SIGKILL) 2558 | time.sleep(MinimumYield) 2559 | # useKillMode in [ "control-group", "mixed", "process" ] 2560 | if pid_exists(mainpid): 2561 | logg.info("hard kill PID %s", mainpid) 2562 | self._kill_pid(mainpid, signal.SIGKILL) 2563 | time.sleep(MinimumYield) 2564 | dead = not pid_exists(mainpid) or pid_zombie(mainpid) 2565 | logg.info("done hard kill PID %s %s", mainpid, dead and "OK") 2566 | return dead 2567 | def _kill_pid(self, pid, kill_signal = None): 2568 | try: 2569 | sig = kill_signal or signal.SIGTERM 2570 | os.kill(pid, sig) 2571 | except OSError as e: 2572 | if e.errno == errno.ESRCH or e.errno == errno.ENOENT: 2573 | logg.debug("kill PID %s => No such process", pid) 2574 | return True 2575 | else: 2576 | logg.error("kill PID %s => %s", pid, str(e)) 2577 | return False 2578 | return not pid_exists(pid) or pid_zombie(pid) 2579 | def is_active_modules(self, *modules): 2580 | """ [UNIT].. -- check if these units are in active state 2581 | implements True if all is-active = True """ 2582 | # systemctl returns multiple lines, one for each argument 2583 | # "active" when is_active 2584 | # "inactive" when not is_active 2585 | # "unknown" when not enabled 2586 | # The return code is set to 2587 | # 0 when "active" 2588 | # 1 when unit is not found 2589 | # 3 when any "inactive" or "unknown" 2590 | # However: # TODO!!!!! BUG in original systemctl!! 2591 | # documentation says " exit code 0 if at least one is active" 2592 | # and "Unless --quiet is specified, print the unit state" 2593 | units = [] 2594 | results = [] 2595 | for module in modules: 2596 | units = self.match_units([ module ]) 2597 | if not units: 2598 | logg.error("Unit %s could not be found.", unit_of(module)) 2599 | results += [ "unknown" ] 2600 | continue 2601 | for unit in units: 2602 | active = self.get_active_unit(unit) 2603 | enabled = self.enabled_unit(unit) 2604 | if enabled != "enabled": active = "unknown" 2605 | results += [ active ] 2606 | break 2607 | ## how it should work: 2608 | status = "active" in results 2609 | ## how 'systemctl' works: 2610 | non_active = [ result for result in results if result != "active" ] 2611 | status = not non_active 2612 | if not status: 2613 | status = 3 2614 | if not _quiet: 2615 | return status, results 2616 | else: 2617 | return status 2618 | def is_active_from(self, conf): 2619 | """ used in try-restart/other commands to check if needed. """ 2620 | if not conf: return False 2621 | return self.get_active_from(conf) == "active" 2622 | def active_pid_from(self, conf): 2623 | if not conf: return False 2624 | pid = self.read_mainpid_from(conf, "") 2625 | return self.is_active_pid(pid) 2626 | def is_active_pid(self, pid): 2627 | """ returns pid if the pid is still an active process """ 2628 | if pid and pid_exists(pid) and not pid_zombie(pid): 2629 | return pid # usually a string (not null) 2630 | return None 2631 | def get_active_unit(self, unit): 2632 | """ returns 'active' 'inactive' 'failed' 'unknown' """ 2633 | conf = self.get_unit_conf(unit) 2634 | if not conf.loaded(): 2635 | logg.warning("Unit %s could not be found.", unit) 2636 | return "unknown" 2637 | return self.get_active_from(conf) 2638 | def get_active_from(self, conf): 2639 | """ returns 'active' 'inactive' 'failed' 'unknown' """ 2640 | # used in try-restart/other commands to check if needed. 2641 | if not conf: return "unknown" 2642 | pid_file = self.pid_file_from(conf) 2643 | if pid_file: # application PIDFile 2644 | if not os.path.exists(pid_file): 2645 | return "inactive" 2646 | status_file = self.status_file_from(conf) 2647 | if self.getsize(status_file): 2648 | state = self.get_status_from(conf, "ActiveState", "") 2649 | if state: 2650 | logg.info("get_status_from %s => %s", conf.name(), state) 2651 | return state 2652 | pid = self.read_mainpid_from(conf, "") 2653 | logg.debug("pid_file '%s' => PID %s", pid_file or status_file, pid) 2654 | if pid: 2655 | if not pid_exists(pid) or pid_zombie(pid): 2656 | return "failed" 2657 | return "active" 2658 | else: 2659 | return "inactive" 2660 | def get_substate_from(self, conf): 2661 | """ returns 'running' 'exited' 'dead' 'failed' 'plugged' 'mounted' """ 2662 | if not conf: return False 2663 | pid_file = self.pid_file_from(conf) 2664 | if pid_file: 2665 | if not os.path.exists(pid_file): 2666 | return "dead" 2667 | status_file = self.status_file_from(conf) 2668 | if self.getsize(status_file): 2669 | state = self.get_status_from(conf, "ActiveState", "") 2670 | if state: 2671 | if state in [ "active" ]: 2672 | return self.get_status_from(conf, "SubState", "running") 2673 | else: 2674 | return self.get_status_from(conf, "SubState", "dead") 2675 | pid = self.read_mainpid_from(conf, "") 2676 | logg.debug("pid_file '%s' => PID %s", pid_file or status_file, pid) 2677 | if pid: 2678 | if not pid_exists(pid) or pid_zombie(pid): 2679 | return "failed" 2680 | return "running" 2681 | else: 2682 | return "dead" 2683 | def is_failed_modules(self, *modules): 2684 | """ [UNIT]... -- check if these units are in failes state 2685 | implements True if any is-active = True """ 2686 | units = [] 2687 | results = [] 2688 | for module in modules: 2689 | units = self.match_units([ module ]) 2690 | if not units: 2691 | logg.error("Unit %s could not be found.", unit_of(module)) 2692 | results += [ "unknown" ] 2693 | continue 2694 | for unit in units: 2695 | active = self.get_active_unit(unit) 2696 | enabled = self.enabled_unit(unit) 2697 | if enabled != "enabled": active = "unknown" 2698 | results += [ active ] 2699 | break 2700 | status = "failed" in results 2701 | if not _quiet: 2702 | return status, results 2703 | else: 2704 | return status 2705 | def is_failed_from(self, conf): 2706 | if conf is None: return True 2707 | return self.get_active_from(conf) == "failed" 2708 | def reset_failed_modules(self, *modules): 2709 | """ [UNIT]... -- Reset failed state for all, one, or more units """ 2710 | units = [] 2711 | status = True 2712 | for module in modules: 2713 | units = self.match_units([ module ]) 2714 | if not units: 2715 | logg.error("Unit %s could not be found.", unit_of(module)) 2716 | return 1 2717 | for unit in units: 2718 | if not self.reset_failed_unit(unit): 2719 | logg.error("Unit %s could not be reset.", unit_of(module)) 2720 | status = False 2721 | break 2722 | return status 2723 | def reset_failed_unit(self, unit): 2724 | conf = self.get_unit_conf(unit) 2725 | if not conf.loaded(): 2726 | logg.warning("Unit %s could not be found.", unit) 2727 | return False 2728 | if self.not_user_conf(conf): 2729 | logg.error("Unit %s not for --user mode", unit) 2730 | return False 2731 | return self.reset_failed_from(conf) 2732 | def reset_failed_from(self, conf): 2733 | if conf is None: return True 2734 | if not self.is_failed_from(conf): return False 2735 | done = False 2736 | status_file = self.status_file_from(conf) 2737 | if status_file and os.path.exists(status_file): 2738 | try: 2739 | os.remove(status_file) 2740 | done = True 2741 | logg.debug("done rm %s", status_file) 2742 | except Exception as e: 2743 | logg.error("while rm %s: %s", status_file, e) 2744 | pid_file = self.pid_file_from(conf) 2745 | if pid_file and os.path.exists(pid_file): 2746 | try: 2747 | os.remove(pid_file) 2748 | done = True 2749 | logg.debug("done rm %s", pid_file) 2750 | except Exception as e: 2751 | logg.error("while rm %s: %s", pid_file, e) 2752 | return done 2753 | def status_modules(self, *modules): 2754 | """ [UNIT]... check the status of these units. 2755 | """ 2756 | found_all = True 2757 | units = [] 2758 | for module in modules: 2759 | matched = self.match_units([ module ]) 2760 | if not matched: 2761 | logg.error("Unit %s could not be found.", unit_of(module)) 2762 | found_all = False 2763 | continue 2764 | for unit in matched: 2765 | if unit not in units: 2766 | units += [ unit ] 2767 | status, result = self.status_units(units) 2768 | if not found_all: 2769 | status = 3 # same as (dead) # original behaviour 2770 | return (status, result) 2771 | def status_units(self, units): 2772 | """ concatenates the status output of all units 2773 | and the last non-successful statuscode """ 2774 | status, result = 0, "" 2775 | for unit in units: 2776 | status1, result1 = self.status_unit(unit) 2777 | if status1: status = status1 2778 | if result: result += "\n\n" 2779 | result += result1 2780 | return status, result 2781 | def status_unit(self, unit): 2782 | conf = self.get_unit_conf(unit) 2783 | result = "%s - %s" % (unit, self.get_description_from(conf)) 2784 | loaded = conf.loaded() 2785 | if loaded: 2786 | filename = conf.filename() 2787 | enabled = self.enabled_from(conf) 2788 | result += "\n Loaded: {loaded} ({filename}, {enabled})".format(**locals()) 2789 | for path in conf.overrides(): 2790 | result += "\n Drop-In: {path}".format(**locals()) 2791 | else: 2792 | result += "\n Loaded: failed" 2793 | return 3, result 2794 | active = self.get_active_from(conf) 2795 | substate = self.get_substate_from(conf) 2796 | result += "\n Active: {} ({})".format(active, substate) 2797 | if active == "active": 2798 | return 0, result 2799 | else: 2800 | return 3, result 2801 | def cat_modules(self, *modules): 2802 | """ [UNIT]... show the *.system file for these" 2803 | """ 2804 | found_all = True 2805 | units = [] 2806 | for module in modules: 2807 | matched = self.match_units([ module ]) 2808 | if not matched: 2809 | logg.error("Unit %s could not be found.", unit_of(module)) 2810 | found_all = False 2811 | continue 2812 | for unit in matched: 2813 | if unit not in units: 2814 | units += [ unit ] 2815 | done, result = self.cat_units(units) 2816 | return (done and found_all, result) 2817 | def cat_units(self, units): 2818 | done = True 2819 | result = "" 2820 | for unit in units: 2821 | text = self.cat_unit(unit) 2822 | if not text: 2823 | done = False 2824 | else: 2825 | if result: 2826 | result += "\n\n" 2827 | result += text 2828 | return done, result 2829 | def cat_unit(self, unit): 2830 | try: 2831 | unit_file = self.unit_file(unit) 2832 | if unit_file: 2833 | return open(unit_file).read() 2834 | logg.error("no file for unit '%s'", unit) 2835 | except Exception as e: 2836 | print("Unit {} is not-loaded: {}".format(unit, e)) 2837 | return False 2838 | ## 2839 | ## 2840 | def load_preset_files(self, module = None): # -> [ preset-file-names,... ] 2841 | """ reads all preset files, returns the scanned files """ 2842 | if self._preset_file_list is None: 2843 | self._preset_file_list = {} 2844 | for folder in self.preset_folders(): 2845 | if not folder: 2846 | continue 2847 | if self._root: 2848 | folder = os_path(self._root, folder) 2849 | if not os.path.isdir(folder): 2850 | continue 2851 | for name in os.listdir(folder): 2852 | if not name.endswith(".preset"): 2853 | continue 2854 | if name not in self._preset_file_list: 2855 | path = os.path.join(folder, name) 2856 | if os.path.isdir(path): 2857 | continue 2858 | preset = PresetFile().read(path) 2859 | self._preset_file_list[name] = preset 2860 | logg.debug("found %s preset files", len(self._preset_file_list)) 2861 | return sorted(self._preset_file_list.keys()) 2862 | def get_preset_of_unit(self, unit): 2863 | """ [UNIT] check the *.preset of this unit 2864 | """ 2865 | self.load_preset_files() 2866 | for filename in sorted(self._preset_file_list.keys()): 2867 | preset = self._preset_file_list[filename] 2868 | status = preset.get_preset(unit) 2869 | if status: 2870 | return status 2871 | return None 2872 | def preset_modules(self, *modules): 2873 | """ [UNIT]... -- set 'enabled' when in *.preset 2874 | """ 2875 | if self.user_mode(): 2876 | logg.warning("preset makes no sense in --user mode") 2877 | return True 2878 | found_all = True 2879 | units = [] 2880 | for module in modules: 2881 | matched = self.match_units([ module ]) 2882 | if not matched: 2883 | logg.error("Unit %s could not be found.", unit_of(module)) 2884 | found_all = False 2885 | continue 2886 | for unit in matched: 2887 | if unit not in units: 2888 | units += [ unit ] 2889 | return self.preset_units(units) and found_all 2890 | def preset_units(self, units): 2891 | """ fails if any unit could not be changed """ 2892 | fails = 0 2893 | found = 0 2894 | for unit in units: 2895 | status = self.get_preset_of_unit(unit) 2896 | if not status: continue 2897 | found += 1 2898 | if status.startswith("enable"): 2899 | if self._preset_mode == "disable": continue 2900 | logg.info("preset enable %s", unit) 2901 | if not self.enable_unit(unit): 2902 | logg.warning("failed to enable %s", unit) 2903 | fails += 1 2904 | if status.startswith("disable"): 2905 | if self._preset_mode == "enable": continue 2906 | logg.info("preset disable %s", unit) 2907 | if not self.disable_unit(unit): 2908 | logg.warning("failed to disable %s", unit) 2909 | fails += 1 2910 | return not fails and not not found 2911 | def system_preset_all(self, *modules): 2912 | """ 'preset' all services 2913 | enable or disable services according to *.preset files 2914 | """ 2915 | if self.user_mode(): 2916 | logg.warning("preset-all makes no sense in --user mode") 2917 | return True 2918 | found_all = True 2919 | units = self.match_units() # TODO: how to handle module arguments 2920 | return self.preset_units(units) and found_all 2921 | def wanted_from(self, conf, default = None): 2922 | if not conf: return default 2923 | return conf.get("Install", "WantedBy", default, True) 2924 | def enablefolders(self, wanted): 2925 | if self.user_mode(): 2926 | for folder in self.user_folders(): 2927 | yield self.default_enablefolder(wanted, folder) 2928 | if True: 2929 | for folder in self.system_folders(): 2930 | yield self.default_enablefolder(wanted, folder) 2931 | def enablefolder(self, wanted = None): 2932 | if self.user_mode(): 2933 | user_folder = self.user_folder() 2934 | return self.default_enablefolder(wanted, user_folder) 2935 | else: 2936 | return self.default_enablefolder(wanted) 2937 | def default_enablefolder(self, wanted = None, basefolder = None): 2938 | basefolder = basefolder or self.system_folder() 2939 | if not wanted: 2940 | return wanted 2941 | if not wanted.endswith(".wants"): 2942 | wanted = wanted + ".wants" 2943 | return os.path.join(basefolder, wanted) 2944 | def enable_modules(self, *modules): 2945 | """ [UNIT]... -- enable these units """ 2946 | found_all = True 2947 | units = [] 2948 | for module in modules: 2949 | matched = self.match_units([ module ]) 2950 | if not matched: 2951 | logg.error("Unit %s could not be found.", unit_of(module)) 2952 | found_all = False 2953 | continue 2954 | for unit in matched: 2955 | logg.info("matched %s", unit) #++ 2956 | if unit not in units: 2957 | units += [ unit ] 2958 | return self.enable_units(units) and found_all 2959 | def enable_units(self, units): 2960 | done = True 2961 | for unit in units: 2962 | if not self.enable_unit(unit): 2963 | done = False 2964 | elif self._now: 2965 | self.start_unit(unit) 2966 | return done 2967 | def enable_unit(self, unit): 2968 | unit_file = self.unit_file(unit) 2969 | if not unit_file: 2970 | logg.error("Unit %s could not be found.", unit) 2971 | return False 2972 | if self.is_sysv_file(unit_file): 2973 | if self.user_mode(): 2974 | logg.error("Initscript %s not for --user mode", unit) 2975 | return False 2976 | return self.enable_unit_sysv(unit_file) 2977 | conf = self.get_unit_conf(unit) 2978 | if self.not_user_conf(conf): 2979 | logg.error("Unit %s not for --user mode", unit) 2980 | return False 2981 | wanted = self.wanted_from(self.get_unit_conf(unit)) 2982 | if not wanted: 2983 | return False # "static" is-enabled 2984 | folder = self.enablefolder(wanted) 2985 | if self._root: 2986 | folder = os_path(self._root, folder) 2987 | if not os.path.isdir(folder): 2988 | os.makedirs(folder) 2989 | target = os.path.join(folder, os.path.basename(unit_file)) 2990 | if True: 2991 | _f = self._force and "-f" or "" 2992 | logg.info("ln -s {_f} '{unit_file}' '{target}'".format(**locals())) 2993 | if self._force and os.path.islink(target): 2994 | os.remove(target) 2995 | if not os.path.islink(target): 2996 | os.symlink(unit_file, target) 2997 | return True 2998 | def rc3_root_folder(self): 2999 | old_folder = "/etc/rc3.d" 3000 | new_folder = "/etc/init.d/rc3.d" 3001 | if self._root: 3002 | old_folder = os_path(self._root, old_folder) 3003 | new_folder = os_path(self._root, new_folder) 3004 | if os.path.isdir(old_folder): 3005 | return old_folder 3006 | return new_folder 3007 | def rc5_root_folder(self): 3008 | old_folder = "/etc/rc5.d" 3009 | new_folder = "/etc/init.d/rc5.d" 3010 | if self._root: 3011 | old_folder = os_path(self._root, old_folder) 3012 | new_folder = os_path(self._root, new_folder) 3013 | if os.path.isdir(old_folder): 3014 | return old_folder 3015 | return new_folder 3016 | def enable_unit_sysv(self, unit_file): 3017 | # a "multi-user.target"/rc3 is also started in /rc5 3018 | rc3 = self._enable_unit_sysv(unit_file, self.rc3_root_folder()) 3019 | rc5 = self._enable_unit_sysv(unit_file, self.rc5_root_folder()) 3020 | return rc3 and rc5 3021 | def _enable_unit_sysv(self, unit_file, rc_folder): 3022 | name = os.path.basename(unit_file) 3023 | nameS = "S50"+name 3024 | nameK = "K50"+name 3025 | if not os.path.isdir(rc_folder): 3026 | os.makedirs(rc_folder) 3027 | # do not double existing entries 3028 | for found in os.listdir(rc_folder): 3029 | m = re.match(r"S\d\d(.*)", found) 3030 | if m and m.group(1) == name: 3031 | nameS = found 3032 | m = re.match(r"K\d\d(.*)", found) 3033 | if m and m.group(1) == name: 3034 | nameK = found 3035 | target = os.path.join(rc_folder, nameS) 3036 | if not os.path.exists(target): 3037 | os.symlink(unit_file, target) 3038 | target = os.path.join(rc_folder, nameK) 3039 | if not os.path.exists(target): 3040 | os.symlink(unit_file, target) 3041 | return True 3042 | def disable_modules(self, *modules): 3043 | """ [UNIT]... -- disable these units """ 3044 | found_all = True 3045 | units = [] 3046 | for module in modules: 3047 | matched = self.match_units([ module ]) 3048 | if not matched: 3049 | logg.error("Unit %s could not be found.", unit_of(module)) 3050 | found_all = False 3051 | continue 3052 | for unit in matched: 3053 | if unit not in units: 3054 | units += [ unit ] 3055 | return self.disable_units(units) and found_all 3056 | def disable_units(self, units): 3057 | done = True 3058 | for unit in units: 3059 | if not self.disable_unit(unit): 3060 | done = False 3061 | return done 3062 | def disable_unit(self, unit): 3063 | unit_file = self.unit_file(unit) 3064 | if not unit_file: 3065 | logg.error("Unit %s could not be found.", unit) 3066 | return False 3067 | if self.is_sysv_file(unit_file): 3068 | if self.user_mode(): 3069 | logg.error("Initscript %s not for --user mode", unit) 3070 | return False 3071 | return self.disable_unit_sysv(unit_file) 3072 | conf = self.get_unit_conf(unit) 3073 | if self.not_user_conf(conf): 3074 | logg.error("Unit %s not for --user mode", unit) 3075 | return False 3076 | wanted = self.wanted_from(self.get_unit_conf(unit)) 3077 | if not wanted: 3078 | return False # "static" is-enabled 3079 | for folder in self.enablefolders(wanted): 3080 | if self._root: 3081 | folder = os_path(self._root, folder) 3082 | target = os.path.join(folder, os.path.basename(unit_file)) 3083 | if os.path.isfile(target): 3084 | try: 3085 | _f = self._force and "-f" or "" 3086 | logg.info("rm {_f} '{target}'".format(**locals())) 3087 | os.remove(target) 3088 | except IOError as e: 3089 | logg.error("disable %s: %s", target, e) 3090 | except OSError as e: 3091 | logg.error("disable %s: %s", target, e) 3092 | return True 3093 | def disable_unit_sysv(self, unit_file): 3094 | rc3 = self._disable_unit_sysv(unit_file, self.rc3_root_folder()) 3095 | rc5 = self._disable_unit_sysv(unit_file, self.rc5_root_folder()) 3096 | return rc3 and rc5 3097 | def _disable_unit_sysv(self, unit_file, rc_folder): 3098 | # a "multi-user.target"/rc3 is also started in /rc5 3099 | name = os.path.basename(unit_file) 3100 | nameS = "S50"+name 3101 | nameK = "K50"+name 3102 | # do not forget the existing entries 3103 | for found in os.listdir(rc_folder): 3104 | m = re.match(r"S\d\d(.*)", found) 3105 | if m and m.group(1) == name: 3106 | nameS = found 3107 | m = re.match(r"K\d\d(.*)", found) 3108 | if m and m.group(1) == name: 3109 | nameK = found 3110 | target = os.path.join(rc_folder, nameS) 3111 | if os.path.exists(target): 3112 | os.unlink(target) 3113 | target = os.path.join(rc_folder, nameK) 3114 | if os.path.exists(target): 3115 | os.unlink(target) 3116 | return True 3117 | def is_enabled_sysv(self, unit_file): 3118 | name = os.path.basename(unit_file) 3119 | target = os.path.join(self.rc3_root_folder(), "S50%s" % name) 3120 | if os.path.exists(target): 3121 | return True 3122 | return False 3123 | def is_enabled_modules(self, *modules): 3124 | """ [UNIT]... -- check if these units are enabled 3125 | returns True if any of them is enabled.""" 3126 | found_all = True 3127 | units = [] 3128 | for module in modules: 3129 | matched = self.match_units([ module ]) 3130 | if not matched: 3131 | logg.error("Unit %s could not be found.", unit_of(module)) 3132 | found_all = False 3133 | continue 3134 | for unit in matched: 3135 | if unit not in units: 3136 | units += [ unit ] 3137 | return self.is_enabled_units(units) # and found_all 3138 | def is_enabled_units(self, units): 3139 | """ true if any is enabled, and a list of infos """ 3140 | result = False 3141 | infos = [] 3142 | for unit in units: 3143 | infos += [ self.enabled_unit(unit) ] 3144 | if self.is_enabled(unit): 3145 | result = True 3146 | return result, infos 3147 | def is_enabled(self, unit): 3148 | unit_file = self.unit_file(unit) 3149 | if not unit_file: 3150 | logg.error("Unit %s could not be found.", unit) 3151 | return False 3152 | if self.is_sysv_file(unit_file): 3153 | return self.is_enabled_sysv(unit_file) 3154 | wanted = self.wanted_from(self.get_unit_conf(unit)) 3155 | if not wanted: 3156 | return True # "static" 3157 | for folder in self.enablefolders(wanted): 3158 | if self._root: 3159 | folder = os_path(self._root, folder) 3160 | target = os.path.join(folder, os.path.basename(unit_file)) 3161 | if os.path.isfile(target): 3162 | return True 3163 | return False 3164 | def enabled_unit(self, unit): 3165 | conf = self.get_unit_conf(unit) 3166 | return self.enabled_from(conf) 3167 | def enabled_from(self, conf): 3168 | unit_file = conf.filename() 3169 | if self.is_sysv_file(unit_file): 3170 | state = self.is_enabled_sysv(unit_file) 3171 | if state: 3172 | return "enabled" 3173 | return "disabled" 3174 | if conf.masked: 3175 | return "masked" 3176 | wanted = self.wanted_from(conf) 3177 | if not wanted: 3178 | return "static" 3179 | for folder in self.enablefolders(wanted): 3180 | if self._root: 3181 | folder = os_path(self._root, folder) 3182 | target = os.path.join(folder, os.path.basename(unit_file)) 3183 | if os.path.isfile(target): 3184 | return "enabled" 3185 | return "disabled" 3186 | def mask_modules(self, *modules): 3187 | """ [UNIT]... -- mask non-startable units """ 3188 | found_all = True 3189 | units = [] 3190 | for module in modules: 3191 | matched = self.match_units([ module ]) 3192 | if not matched: 3193 | logg.error("Unit %s could not be found.", unit_of(module)) 3194 | found_all = False 3195 | continue 3196 | for unit in matched: 3197 | if unit not in units: 3198 | units += [ unit ] 3199 | return self.mask_units(units) and found_all 3200 | def mask_units(self, units): 3201 | done = True 3202 | for unit in units: 3203 | if not self.mask_unit(unit): 3204 | done = False 3205 | return done 3206 | def mask_unit(self, unit): 3207 | unit_file = self.unit_file(unit) 3208 | if not unit_file: 3209 | logg.error("Unit %s could not be found.", unit) 3210 | return False 3211 | if self.is_sysv_file(unit_file): 3212 | logg.error("Initscript %s can not be masked", unit) 3213 | return False 3214 | conf = self.get_unit_conf(unit) 3215 | if self.not_user_conf(conf): 3216 | logg.error("Unit %s not for --user mode", unit) 3217 | return False 3218 | folder = self.mask_folder() 3219 | if self._root: 3220 | folder = os_path(self._root, folder) 3221 | if not os.path.isdir(folder): 3222 | os.makedirs(folder) 3223 | target = os.path.join(folder, os.path.basename(unit_file)) 3224 | if True: 3225 | _f = self._force and "-f" or "" 3226 | logg.debug("ln -s {_f} /dev/null '{target}'".format(**locals())) 3227 | if self._force and os.path.islink(target): 3228 | os.remove(target) 3229 | if not os.path.exists(target): 3230 | os.symlink("/dev/null", target) 3231 | logg.info("Created symlink {target} -> /dev/null".format(**locals())) 3232 | return True 3233 | elif os.path.islink(target): 3234 | logg.debug("mask symlink does already exist: %s", target) 3235 | return True 3236 | else: 3237 | logg.error("mask target does already exist: %s", target) 3238 | return False 3239 | def mask_folder(self): 3240 | for folder in self.mask_folders(): 3241 | if folder: return folder 3242 | raise Exception("did not find any systemd/system folder") 3243 | def mask_folders(self): 3244 | if self.user_mode(): 3245 | for folder in self.user_folders(): 3246 | yield folder 3247 | if True: 3248 | for folder in self.system_folders(): 3249 | yield folder 3250 | def unmask_modules(self, *modules): 3251 | """ [UNIT]... -- unmask non-startable units """ 3252 | found_all = True 3253 | units = [] 3254 | for module in modules: 3255 | matched = self.match_units([ module ]) 3256 | if not matched: 3257 | logg.error("Unit %s could not be found.", unit_of(module)) 3258 | found_all = False 3259 | continue 3260 | for unit in matched: 3261 | if unit not in units: 3262 | units += [ unit ] 3263 | return self.unmask_units(units) and found_all 3264 | def unmask_units(self, units): 3265 | done = True 3266 | for unit in units: 3267 | if not self.unmask_unit(unit): 3268 | done = False 3269 | return done 3270 | def unmask_unit(self, unit): 3271 | unit_file = self.unit_file(unit) 3272 | if not unit_file: 3273 | logg.error("Unit %s could not be found.", unit) 3274 | return False 3275 | if self.is_sysv_file(unit_file): 3276 | logg.error("Initscript %s can not be un/masked", unit) 3277 | return False 3278 | conf = self.get_unit_conf(unit) 3279 | if self.not_user_conf(conf): 3280 | logg.error("Unit %s not for --user mode", unit) 3281 | return False 3282 | folder = self.mask_folder() 3283 | if self._root: 3284 | folder = os_path(self._root, folder) 3285 | target = os.path.join(folder, os.path.basename(unit_file)) 3286 | if True: 3287 | _f = self._force and "-f" or "" 3288 | logg.info("rm {_f} '{target}'".format(**locals())) 3289 | if os.path.islink(target): 3290 | os.remove(target) 3291 | return True 3292 | elif not os.path.exists(target): 3293 | logg.debug("Symlink did exist anymore: %s", target) 3294 | return True 3295 | else: 3296 | logg.warning("target is not a symlink: %s", target) 3297 | return True 3298 | def list_dependencies_modules(self, *modules): 3299 | """ [UNIT]... show the dependency tree" 3300 | """ 3301 | found_all = True 3302 | units = [] 3303 | for module in modules: 3304 | matched = self.match_units([ module ]) 3305 | if not matched: 3306 | logg.error("Unit %s could not be found.", unit_of(module)) 3307 | found_all = False 3308 | continue 3309 | for unit in matched: 3310 | if unit not in units: 3311 | units += [ unit ] 3312 | return self.list_dependencies_units(units) # and found_all 3313 | def list_dependencies_units(self, units): 3314 | if self._now: 3315 | return self.list_start_dependencies_units(units) 3316 | result = [] 3317 | for unit in units: 3318 | if result: 3319 | result += [ "", "" ] 3320 | result += self.list_dependencies_unit(unit) 3321 | return result 3322 | def list_dependencies_unit(self, unit): 3323 | result = [] 3324 | for line in self.list_dependencies(unit, ""): 3325 | result += [ line ] 3326 | return result 3327 | def list_dependencies(self, unit, indent = None, mark = None, loop = []): 3328 | mapping = {} 3329 | mapping["Requires"] = "required to start" 3330 | mapping["Wants"] = "wanted to start" 3331 | mapping["Requisite"] = "required started" 3332 | mapping["Bindsto"] = "binds to start" 3333 | mapping["PartOf"] = "part of started" 3334 | mapping[".requires"] = ".required to start" 3335 | mapping[".wants"] = ".wanted to start" 3336 | mapping["PropagateReloadTo"] = "(to be reloaded as well)" 3337 | mapping["Conflicts"] = "(to be stopped on conflict)" 3338 | restrict = ["Requires", "Requisite", "ConsistsOf", "Wants", 3339 | "BindsTo", ".requires", ".wants"] 3340 | indent = indent or "" 3341 | mark = mark or "" 3342 | deps = self.get_dependencies_unit(unit) 3343 | conf = self.get_unit_conf(unit) 3344 | if not conf.loaded(): 3345 | if not self._show_all: 3346 | return 3347 | yield "%s(%s): %s" % (indent, unit, mark) 3348 | else: 3349 | yield "%s%s: %s" % (indent, unit, mark) 3350 | for stop_recursion in [ "Conflict", "conflict", "reloaded", "Propagate" ]: 3351 | if stop_recursion in mark: 3352 | return 3353 | for dep in deps: 3354 | if dep in loop: 3355 | logg.debug("detected loop at %s", dep) 3356 | continue 3357 | new_loop = loop + list(deps.keys()) 3358 | new_indent = indent + "| " 3359 | new_mark = deps[dep] 3360 | if not self._show_all: 3361 | if new_mark not in restrict: 3362 | continue 3363 | if new_mark in mapping: 3364 | new_mark = mapping[new_mark] 3365 | restrict = ["Requires", "Requisite", "ConsistsOf", "Wants", 3366 | "BindsTo", ".requires", ".wants"] 3367 | for line in self.list_dependencies(dep, new_indent, new_mark, new_loop): 3368 | yield line 3369 | def get_dependencies_unit(self, unit): 3370 | conf = self.get_unit_conf(unit) 3371 | deps = {} 3372 | for style in [ "Requires", "Wants", "Requisite", "BindsTo", "PartOf", 3373 | ".requires", ".wants", "PropagateReloadTo", "Conflicts", ]: 3374 | if style.startswith("."): 3375 | for folder in self.sysd_folders(): 3376 | if not folder: 3377 | continue 3378 | require_path = os.path.join(folder, unit + style) 3379 | if self._root: 3380 | require_path = os_path(self._root, require_path) 3381 | if os.path.isdir(require_path): 3382 | for required in os.listdir(require_path): 3383 | if required not in deps: 3384 | deps[required] = style 3385 | else: 3386 | for requirelist in conf.getlist("Unit", style, []): 3387 | for required in requirelist.strip().split(" "): 3388 | deps[required.strip()] = style 3389 | return deps 3390 | def get_start_dependencies(self, unit): # pragma: no cover 3391 | """ the list of services to be started as well / TODO: unused """ 3392 | deps = {} 3393 | unit_deps = self.get_dependencies_unit(unit) 3394 | for dep_unit, dep_style in unit_deps.items(): 3395 | restrict = ["Requires", "Requisite", "ConsistsOf", "Wants", 3396 | "BindsTo", ".requires", ".wants"] 3397 | if dep_style in restrict: 3398 | if dep_unit in deps: 3399 | if dep_style not in deps[dep_unit]: 3400 | deps[dep_unit].append( dep_style) 3401 | else: 3402 | deps[dep_unit] = [ dep_style ] 3403 | next_deps = self.get_start_dependencies(dep_unit) 3404 | for dep, styles in next_deps.items(): 3405 | for style in styles: 3406 | if dep in deps: 3407 | if style not in deps[dep]: 3408 | deps[dep].append(style) 3409 | else: 3410 | deps[dep] = [ style ] 3411 | return deps 3412 | def list_start_dependencies_units(self, units): 3413 | unit_order = [] 3414 | deps = {} 3415 | for unit in units: 3416 | unit_order.append(unit) 3417 | # unit_deps = self.get_start_dependencies(unit) # TODO 3418 | unit_deps = self.get_dependencies_unit(unit) 3419 | for dep_unit, styles in unit_deps.items(): 3420 | styles = to_list(styles) 3421 | for dep_style in styles: 3422 | if dep_unit in deps: 3423 | if dep_style not in deps[dep_unit]: 3424 | deps[dep_unit].append( dep_style) 3425 | else: 3426 | deps[dep_unit] = [ dep_style ] 3427 | deps_conf = [] 3428 | for dep in deps: 3429 | if dep in unit_order: 3430 | continue 3431 | conf = self.get_unit_conf(dep) 3432 | if conf.loaded(): 3433 | deps_conf.append(conf) 3434 | for unit in unit_order: 3435 | deps[unit] = [ "Requested" ] 3436 | conf = self.get_unit_conf(unit) 3437 | if conf.loaded(): 3438 | deps_conf.append(conf) 3439 | result = [] 3440 | for dep in sortedAfter(deps_conf, cmp=compareAfter): 3441 | line = (dep.name(), "(%s)" % (" ".join(deps[dep.name()]))) 3442 | result.append(line) 3443 | return result 3444 | def sortedAfter(self, unitlist): 3445 | """ get correct start order for the unit list (ignoring masked units) """ 3446 | conflist = [ self.get_unit_conf(unit) for unit in unitlist ] 3447 | if True: 3448 | conflist = [] 3449 | for unit in unitlist: 3450 | conf = self.get_unit_conf(unit) 3451 | if conf.masked: 3452 | logg.debug("ignoring masked unit %s", unit) 3453 | continue 3454 | conflist.append(conf) 3455 | sortlist = sortedAfter(conflist) 3456 | return [ item.name() for item in sortlist ] 3457 | def sortedBefore(self, unitlist): 3458 | """ get correct start order for the unit list (ignoring masked units) """ 3459 | conflist = [ self.get_unit_conf(unit) for unit in unitlist ] 3460 | if True: 3461 | conflist = [] 3462 | for unit in unitlist: 3463 | conf = self.get_unit_conf(unit) 3464 | if conf.masked: 3465 | logg.debug("ignoring masked unit %s", unit) 3466 | continue 3467 | conflist.append(conf) 3468 | sortlist = sortedAfter(reversed(conflist)) 3469 | return [ item.name() for item in reversed(sortlist) ] 3470 | def system_daemon_reload(self): 3471 | """ reload does will only check the service files here. 3472 | The returncode will tell the number of warnings, 3473 | and it is over 100 if it can not continue even 3474 | for the relaxed systemctl.py style of execution. """ 3475 | errors = 0 3476 | for unit in self.match_units(): 3477 | try: 3478 | conf = self.get_unit_conf(unit) 3479 | except Exception as e: 3480 | logg.error("%s: can not read unit file %s\n\t%s", 3481 | unit, conf.filename(), e) 3482 | continue 3483 | errors += self.syntax_check(conf) 3484 | if errors: 3485 | logg.warning(" (%s) found %s problems", errors, errors % 100) 3486 | return True # errors 3487 | def syntax_check(self, conf): 3488 | if conf.filename() and conf.filename().endswith(".service"): 3489 | return self.syntax_check_service(conf) 3490 | return 0 3491 | def syntax_check_service(self, conf): 3492 | unit = conf.name() 3493 | if not conf.data.has_section("Service"): 3494 | logg.error(" %s: a .service file without [Service] section", unit) 3495 | return 101 3496 | errors = 0 3497 | haveType = conf.get("Service", "Type", "simple") 3498 | haveExecStart = conf.getlist("Service", "ExecStart", []) 3499 | haveExecStop = conf.getlist("Service", "ExecStop", []) 3500 | haveExecReload = conf.getlist("Service", "ExecReload", []) 3501 | usedExecStart = [] 3502 | usedExecStop = [] 3503 | usedExecReload = [] 3504 | if haveType not in [ "simple", "forking", "notify", "oneshot", "dbus", "idle", "sysv"]: 3505 | logg.error(" %s: Failed to parse service type, ignoring: %s", unit, haveType) 3506 | errors += 100 3507 | for line in haveExecStart: 3508 | if not line.startswith("/") and not line.startswith("-/"): 3509 | logg.error(" %s: Executable path is not absolute, ignoring: %s", unit, line.strip()) 3510 | errors += 1 3511 | usedExecStart.append(line) 3512 | for line in haveExecStop: 3513 | if not line.startswith("/") and not line.startswith("-/"): 3514 | logg.error(" %s: Executable path is not absolute, ignoring: %s", unit, line.strip()) 3515 | errors += 1 3516 | usedExecStop.append(line) 3517 | for line in haveExecReload: 3518 | if not line.startswith("/") and not line.startswith("-/"): 3519 | logg.error(" %s: Executable path is not absolute, ignoring: %s", unit, line.strip()) 3520 | errors += 1 3521 | usedExecReload.append(line) 3522 | if haveType in ["simple", "notify", "forking"]: 3523 | if not usedExecStart and not usedExecStop: 3524 | logg.error(" %s: Service lacks both ExecStart and ExecStop= setting. Refusing.", unit) 3525 | errors += 101 3526 | elif not usedExecStart and haveType != "oneshot": 3527 | logg.error(" %s: Service has no ExecStart= setting, which is only allowed for Type=oneshot services. Refusing.", unit) 3528 | errors += 101 3529 | if len(usedExecStart) > 1 and haveType != "oneshot": 3530 | logg.error(" %s: there may be only one ExecStart statement (unless for 'oneshot' services)." 3531 | + "\n\t\t\tYou can use ExecStartPre / ExecStartPost to add additional commands.", unit) 3532 | errors += 1 3533 | if len(usedExecStop) > 1 and haveType != "oneshot": 3534 | logg.info(" %s: there should be only one ExecStop statement (unless for 'oneshot' services)." 3535 | + "\n\t\t\tYou can use ExecStopPost to add additional commands (also executed on failed Start)", unit) 3536 | if len(usedExecReload) > 1: 3537 | logg.info(" %s: there should be only one ExecReload statement." 3538 | + "\n\t\t\tUse ' ; ' for multiple commands (ExecReloadPost or ExedReloadPre do not exist)", unit) 3539 | if len(usedExecReload) > 0 and "/bin/kill " in usedExecReload[0]: 3540 | logg.warning(" %s: the use of /bin/kill is not recommended for ExecReload as it is asychronous." 3541 | + "\n\t\t\tThat means all the dependencies will perform the reload simultanously / out of order.", unit) 3542 | if conf.getlist("Service", "ExecRestart", []): #pragma: no cover 3543 | logg.error(" %s: there no such thing as an ExecRestart (ignored)", unit) 3544 | if conf.getlist("Service", "ExecRestartPre", []): #pragma: no cover 3545 | logg.error(" %s: there no such thing as an ExecRestartPre (ignored)", unit) 3546 | if conf.getlist("Service", "ExecRestartPost", []): #pragma: no cover 3547 | logg.error(" %s: there no such thing as an ExecRestartPost (ignored)", unit) 3548 | if conf.getlist("Service", "ExecReloadPre", []): #pragma: no cover 3549 | logg.error(" %s: there no such thing as an ExecReloadPre (ignored)", unit) 3550 | if conf.getlist("Service", "ExecReloadPost", []): #pragma: no cover 3551 | logg.error(" %s: there no such thing as an ExecReloadPost (ignored)", unit) 3552 | if conf.getlist("Service", "ExecStopPre", []): #pragma: no cover 3553 | logg.error(" %s: there no such thing as an ExecStopPre (ignored)", unit) 3554 | for env_file in conf.getlist("Service", "EnvironmentFile", []): 3555 | if env_file.startswith("-"): continue 3556 | if not os.path.isfile(os_path(self._root, env_file)): 3557 | logg.error(" %s: Failed to load environment files: %s", unit, env_file) 3558 | errors += 101 3559 | return errors 3560 | def exec_check_service(self, conf, env, exectype = ""): 3561 | if not conf: 3562 | return True 3563 | if not conf.data.has_section("Service"): 3564 | return True #pragma: no cover 3565 | haveType = conf.get("Service", "Type", "simple") 3566 | if haveType in [ "sysv" ]: 3567 | return True # we don't care about that 3568 | abspath = 0 3569 | notexists = 0 3570 | for execs in [ "ExecStartPre", "ExecStart", "ExecStartPost", "ExecStop", "ExecStopPost", "ExecReload" ]: 3571 | if not execs.startswith(exectype): 3572 | continue 3573 | for cmd in conf.getlist("Service", execs, []): 3574 | check, cmd = checkstatus(cmd) 3575 | newcmd = self.exec_cmd(cmd, env, conf) 3576 | if not newcmd: 3577 | continue 3578 | exe = newcmd[0] 3579 | if not exe: 3580 | continue 3581 | if exe[0] != "/": 3582 | logg.error(" Exec is not an absolute path: %s=%s", execs, cmd) 3583 | abspath += 1 3584 | if not os.path.isfile(exe): 3585 | logg.error(" Exec command does not exist: (%s) %s", execs, exe) 3586 | notexists += 1 3587 | newexe1 = os.path.join("/usr/bin", exe) 3588 | newexe2 = os.path.join("/bin", exe) 3589 | if os.path.exists(newexe1): 3590 | logg.error(" but this does exist: %s %s", " " * len(execs), newexe1) 3591 | elif os.path.exists(newexe2): 3592 | logg.error(" but this does exist: %s %s", " " * len(execs), newexe2) 3593 | if not abspath and not notexists: 3594 | return True 3595 | if True: 3596 | filename = conf.filename() 3597 | if len(filename) > 45: filename = "..." + filename[-42:] 3598 | logg.error(" !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") 3599 | logg.error(" Found %s problems in %s", abspath + notexists, filename) 3600 | time.sleep(1) 3601 | if abspath: 3602 | logg.error(" The SystemD commands must always be absolute paths by definition.") 3603 | time.sleep(1) 3604 | logg.error(" Earlier versions of systemctl.py did use a subshell thus using $PATH") 3605 | time.sleep(1) 3606 | logg.error(" however newer versions use execve just like the real SystemD daemon") 3607 | time.sleep(1) 3608 | logg.error(" so that your docker-only service scripts may start to fail suddenly.") 3609 | time.sleep(1) 3610 | if notexists: 3611 | logg.error(" Now %s executable paths were not found in the current environment.", notexists) 3612 | time.sleep(1) 3613 | logg.error(" !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") 3614 | return False 3615 | def show_modules(self, *modules): 3616 | """ [PATTERN]... -- Show properties of one or more units 3617 | Show properties of one or more units (or the manager itself). 3618 | If no argument is specified, properties of the manager will be 3619 | shown. If a unit name is specified, properties of the unit is 3620 | shown. By default, empty properties are suppressed. Use --all to 3621 | show those too. To select specific properties to show, use 3622 | --property=. This command is intended to be used whenever 3623 | computer-parsable output is required. Use status if you are looking 3624 | for formatted human-readable output. 3625 | 3626 | NOTE: only a subset of properties is implemented """ 3627 | notfound = [] 3628 | found_all = True 3629 | units = [] 3630 | for module in modules: 3631 | matched = self.match_units([ module ]) 3632 | if not matched: 3633 | logg.error("Unit %s could not be found.", unit_of(module)) 3634 | units += [ module ] 3635 | found_all = False 3636 | continue 3637 | for unit in matched: 3638 | if unit not in units: 3639 | units += [ unit ] 3640 | return self.show_units(units) + notfound # and found_all 3641 | def show_units(self, units): 3642 | logg.debug("show --property=%s", self._unit_property) 3643 | result = [] 3644 | for unit in units: 3645 | if result: result += [ "" ] 3646 | for var, value in self.show_unit_items(unit): 3647 | if self._unit_property: 3648 | if self._unit_property != var: 3649 | continue 3650 | else: 3651 | if not value and not self._show_all: 3652 | continue 3653 | result += [ "%s=%s" % (var, value) ] 3654 | return result 3655 | def show_unit_items(self, unit): 3656 | """ [UNIT]... -- show properties of a unit. 3657 | """ 3658 | logg.info("try read unit %s", unit) 3659 | conf = self.get_unit_conf(unit) 3660 | for entry in self.each_unit_items(unit, conf): 3661 | yield entry 3662 | def each_unit_items(self, unit, conf): 3663 | loaded = conf.loaded() 3664 | if not loaded: 3665 | loaded = "not-loaded" 3666 | if "NOT-FOUND" in self.get_description_from(conf): 3667 | loaded = "not-found" 3668 | yield "Id", unit 3669 | yield "Names", unit 3670 | yield "Description", self.get_description_from(conf) # conf.get("Unit", "Description") 3671 | yield "PIDFile", self.pid_file_from(conf) # not self.pid_file_from w/o default location 3672 | yield "MainPID", self.active_pid_from(conf) or "0" # status["MainPID"] or PIDFile-read 3673 | yield "SubState", self.get_substate_from(conf) # status["SubState"] or notify-result 3674 | yield "ActiveState", self.get_active_from(conf) # status["ActiveState"] 3675 | yield "LoadState", loaded 3676 | yield "UnitFileState", self.enabled_from(conf) 3677 | yield "TimeoutStartUSec", seconds_to_time(self.get_TimeoutStartSec(conf)) 3678 | yield "TimeoutStopUSec", seconds_to_time(self.get_TimeoutStopSec(conf)) 3679 | env_parts = [] 3680 | for env_part in conf.getlist("Service", "Environment", []): 3681 | env_parts.append(self.expand_special(env_part, conf)) 3682 | if env_parts: 3683 | yield "Environment", " ".join(env_parts) 3684 | env_files = [] 3685 | for env_file in conf.getlist("Service", "EnvironmentFile", []): 3686 | env_files.append(self.expand_special(env_file, conf)) 3687 | if env_files: 3688 | yield "EnvironmentFile", " ".join(env_files) 3689 | # 3690 | igno_centos = [ "netconsole", "network" ] 3691 | igno_opensuse = [ "raw", "pppoe", "*.local", "boot.*", "rpmconf*", "purge-kernels.service", "after-local.service", "postfix*" ] 3692 | igno_ubuntu = [ "mount*", "umount*", "ondemand", "*.local" ] 3693 | igno_always = [ "network*", "dbus", "systemd-*" ] 3694 | def _ignored_unit(self, unit, ignore_list): 3695 | for ignore in ignore_list: 3696 | if fnmatch.fnmatchcase(unit, ignore): 3697 | return True # ignore 3698 | if fnmatch.fnmatchcase(unit, ignore+".service"): 3699 | return True # ignore 3700 | return False 3701 | def system_default_services(self, sysv = "S", default_target = None): 3702 | """ show the default services 3703 | This is used internally to know the list of service to be started in 'default' 3704 | runlevel when the container is started through default initialisation. It will 3705 | ignore a number of services - use '--all' to show a longer list of services and 3706 | use '--all --force' if not even a minimal filter shall be used. 3707 | """ 3708 | igno = self.igno_centos + self.igno_opensuse + self.igno_ubuntu + self.igno_always 3709 | if self._show_all: 3710 | igno = self.igno_always 3711 | if self._force: 3712 | igno = [] 3713 | logg.debug("ignored services filter for default.target:\n\t%s", igno) 3714 | return self.enabled_default_services(sysv, default_target, igno) 3715 | def enabled_default_services(self, sysv = "S", default_target = None, igno = []): 3716 | if self.user_mode(): 3717 | return self.enabled_default_user_services(sysv, default_target, igno) 3718 | else: 3719 | return self.enabled_default_system_services(sysv, default_target, igno) 3720 | def enabled_default_user_services(self, sysv = "S", default_target = None, igno = []): 3721 | logg.debug("check for default user services") 3722 | default_target = default_target or self._default_target 3723 | default_services = [] 3724 | for basefolder in self.user_folders(): 3725 | if not basefolder: 3726 | continue 3727 | folder = self.default_enablefolder(default_target, basefolder) 3728 | if self._root: 3729 | folder = os_path(self._root, folder) 3730 | if os.path.isdir(folder): 3731 | for unit in sorted(os.listdir(folder)): 3732 | path = os.path.join(folder, unit) 3733 | if os.path.isdir(path): continue 3734 | if self._ignored_unit(unit, igno): 3735 | continue # ignore 3736 | if unit.endswith(".service"): 3737 | default_services.append(unit) 3738 | for basefolder in self.system_folders(): 3739 | if not basefolder: 3740 | continue 3741 | folder = self.default_enablefolder(default_target, basefolder) 3742 | if self._root: 3743 | folder = os_path(self._root, folder) 3744 | if os.path.isdir(folder): 3745 | for unit in sorted(os.listdir(folder)): 3746 | path = os.path.join(folder, unit) 3747 | if os.path.isdir(path): continue 3748 | if self._ignored_unit(unit, igno): 3749 | continue # ignore 3750 | if unit.endswith(".service"): 3751 | conf = self.load_unit_conf(unit) 3752 | if self.not_user_conf(conf): 3753 | pass 3754 | else: 3755 | default_services.append(unit) 3756 | return default_services 3757 | def enabled_default_system_services(self, sysv = "S", default_target = None, igno = []): 3758 | logg.debug("check for default system services") 3759 | default_target = default_target or self._default_target 3760 | default_services = [] 3761 | for basefolder in self.system_folders(): 3762 | if not basefolder: 3763 | continue 3764 | folder = self.default_enablefolder(default_target, basefolder) 3765 | if self._root: 3766 | folder = os_path(self._root, folder) 3767 | if os.path.isdir(folder): 3768 | for unit in sorted(os.listdir(folder)): 3769 | path = os.path.join(folder, unit) 3770 | if os.path.isdir(path): continue 3771 | if self._ignored_unit(unit, igno): 3772 | continue # ignore 3773 | if unit.endswith(".service"): 3774 | default_services.append(unit) 3775 | for folder in [ self.rc3_root_folder() ]: 3776 | if not os.path.isdir(folder): 3777 | logg.warning("non-existant %s", folder) 3778 | continue 3779 | for unit in sorted(os.listdir(folder)): 3780 | path = os.path.join(folder, unit) 3781 | if os.path.isdir(path): continue 3782 | m = re.match(sysv+r"\d\d(.*)", unit) 3783 | if m: 3784 | service = m.group(1) 3785 | unit = service + ".service" 3786 | if self._ignored_unit(unit, igno): 3787 | continue # ignore 3788 | default_services.append(unit) 3789 | return default_services 3790 | def system_default(self, arg = True): 3791 | """ start units for default system level 3792 | This will go through the enabled services in the default 'multi-user.target'. 3793 | However some services are ignored as being known to be installation garbage 3794 | from unintended services. Use '--all' so start all of the installed services 3795 | and with '--all --force' even those services that are otherwise wrong. 3796 | /// SPECIAL: with --now or --init the init-loop is run and afterwards 3797 | a system_halt is performed with the enabled services to be stopped.""" 3798 | logg.info("system default requested - %s", arg) 3799 | init = self._now or self._init 3800 | self.start_system_default(init = init) 3801 | def start_system_default(self, init = False): 3802 | """ detect the default.target services and start them. 3803 | When --init is given then the init-loop is run and 3804 | the services are stopped again by 'systemctl halt'.""" 3805 | default_target = self._default_target 3806 | default_services = self.system_default_services("S", default_target) 3807 | self.start_units(default_services) 3808 | logg.info(" -- system is up") 3809 | if init: 3810 | logg.info("init-loop start") 3811 | sig = self.init_loop_until_stop(default_services) 3812 | logg.info("init-loop %s", sig) 3813 | self.stop_system_default() 3814 | def stop_system_default(self): 3815 | """ detect the default.target services and stop them. 3816 | This is commonly run through 'systemctl halt' or 3817 | at the end of a 'systemctl --init default' loop.""" 3818 | default_target = self._default_target 3819 | default_services = self.system_default_services("K", default_target) 3820 | self.stop_units(default_services) 3821 | logg.info(" -- system is down") 3822 | def system_halt(self, arg = True): 3823 | """ stop units from default system level """ 3824 | logg.info("system halt requested - %s", arg) 3825 | self.stop_system_default() 3826 | try: 3827 | os.kill(1, signal.SIGQUIT) # exit init-loop on no_more_procs 3828 | except Exception as e: 3829 | logg.warning("SIGQUIT to init-loop on PID-1: %s", e) 3830 | def system_get_default(self): 3831 | """ get current default run-level""" 3832 | current = self._default_target 3833 | folder = os_path(self._root, self.mask_folder()) 3834 | target = os.path.join(folder, "default.target") 3835 | if os.path.islink(target): 3836 | current = os.path.basename(os.readlink(target)) 3837 | return current 3838 | def set_default_modules(self, *modules): 3839 | """ set current default run-level""" 3840 | if not modules: 3841 | logg.debug(".. no runlevel given") 3842 | return (1, "Too few arguments") 3843 | current = self._default_target 3844 | folder = os_path(self._root, self.mask_folder()) 3845 | target = os.path.join(folder, "default.target") 3846 | if os.path.islink(target): 3847 | current = os.path.basename(os.readlink(target)) 3848 | err, msg = 0, "" 3849 | for module in modules: 3850 | if module == current: 3851 | continue 3852 | targetfile = None 3853 | for targetname, targetpath in self.each_target_file(): 3854 | if targetname == module: 3855 | targetfile = targetpath 3856 | if not targetfile: 3857 | err, msg = 3, "No such runlevel %s" % (module) 3858 | continue 3859 | # 3860 | if os.path.islink(target): 3861 | os.unlink(target) 3862 | if not os.path.isdir(os.path.dirname(target)): 3863 | os.makedirs(os.path.dirname(target)) 3864 | os.symlink(targetfile, target) 3865 | msg = "Created symlink from %s -> %s" % (target, targetfile) 3866 | logg.debug("%s", msg) 3867 | return (err, msg) 3868 | def init_modules(self, *modules): 3869 | """ [UNIT*] -- init loop: '--init default' or '--init start UNIT*' 3870 | The systemctl init service will start the enabled 'default' services, 3871 | and then wait for any zombies to be reaped. When a SIGINT is received 3872 | then a clean shutdown of the enabled services is ensured. A Control-C in 3873 | in interactive mode will also run 'stop' on all the enabled services. // 3874 | When a UNIT name is given then only that one is started instead of the 3875 | services in the 'default.target'. Using 'init UNIT' is better than 3876 | '--init start UNIT' because the UNIT is also stopped cleanly even when 3877 | it was never enabled in the system. 3878 | /// SPECIAL: when using --now then only the init-loop is started, 3879 | with the reap-zombies function and waiting for an interrupt. 3880 | (and no unit is started/stoppped wether given or not). 3881 | """ 3882 | if self._now: 3883 | return self.init_loop_until_stop([]) 3884 | if not modules: 3885 | # like 'systemctl --init default' 3886 | if self._now or self._show_all: 3887 | logg.debug("init default --now --all => no_more_procs") 3888 | self.exit_when_no_more_procs = True 3889 | return self.start_system_default(init = True) 3890 | # 3891 | # otherwise quit when all the init-services have died 3892 | self.exit_when_no_more_services = True 3893 | if self._now or self._show_all: 3894 | logg.debug("init services --now --all => no_more_procs") 3895 | self.exit_when_no_more_procs = True 3896 | found_all = True 3897 | units = [] 3898 | for module in modules: 3899 | matched = self.match_units([ module ]) 3900 | if not matched: 3901 | logg.error("Unit %s could not be found.", unit_of(module)) 3902 | found_all = False 3903 | continue 3904 | for unit in matched: 3905 | if unit not in units: 3906 | units += [ unit ] 3907 | logg.info("init %s -> start %s", ",".join(modules), ",".join(units)) 3908 | done = self.start_units(units, init = True) 3909 | logg.info("-- init is done") 3910 | return done # and found_all 3911 | def start_log_files(self, units): 3912 | self._log_file = {} 3913 | self._log_hold = {} 3914 | for unit in units: 3915 | conf = self.load_unit_conf(unit) 3916 | if not conf: continue 3917 | log_path = self.path_journal_log(conf) 3918 | try: 3919 | opened = open(log_path) 3920 | fd = opened.fileno() 3921 | fl = fcntl.fcntl(fd, fcntl.F_GETFL) 3922 | fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) 3923 | self._log_file[unit] = opened 3924 | self._log_hold[unit] = "" 3925 | except Exception as e: 3926 | logg.error("can not open %s log: %s\n\t%s", unit, log_path, e) 3927 | def read_log_files(self, units): 3928 | for unit in units: 3929 | if unit in self._log_file: 3930 | new_text = self._log_file[unit].read() 3931 | text = self._log_hold[unit] + new_text 3932 | if not text: continue 3933 | lines = text.split("\n") 3934 | if not text.endswith("\n"): 3935 | self._log_hold[unit] = lines[-1] 3936 | lines = lines[:-1] 3937 | for line in lines: 3938 | content = unit+": "+line+"\n" 3939 | os.write(1, content.encode("utf-8")) 3940 | try: os.fsync(1) 3941 | except: pass 3942 | def stop_log_files(self, units): 3943 | for unit in units: 3944 | try: 3945 | if unit in self._log_file: 3946 | if self._log_file[unit]: 3947 | self._log_file[unit].close() 3948 | except Exception as e: 3949 | logg.error("can not close log: %s\n\t%s", unit, e) 3950 | self._log_file = {} 3951 | self._log_hold = {} 3952 | def init_loop_until_stop(self, units): 3953 | """ this is the init-loop - it checks for any zombies to be reaped and 3954 | waits for an interrupt. When a SIGTERM /SIGINT /Control-C signal 3955 | is received then the signal name is returned. Any other signal will 3956 | just raise an Exception like one would normally expect. As a special 3957 | the 'systemctl halt' emits SIGQUIT which puts it into no_more_procs mode.""" 3958 | signal.signal(signal.SIGQUIT, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt("SIGQUIT")) 3959 | signal.signal(signal.SIGINT, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt("SIGINT")) 3960 | signal.signal(signal.SIGTERM, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt("SIGTERM")) 3961 | self.start_log_files(units) 3962 | result = None 3963 | while True: 3964 | try: 3965 | time.sleep(InitLoopSleep) 3966 | self.read_log_files(units) 3967 | ##### the reaper goes round 3968 | running = self.system_reap_zombies() 3969 | # logg.debug("reap zombies - init-loop found %s running procs", running) 3970 | if self.exit_when_no_more_services: 3971 | active = False 3972 | for unit in units: 3973 | conf = self.load_unit_conf(unit) 3974 | if not conf: continue 3975 | if self.is_active_from(conf): 3976 | active = True 3977 | if not active: 3978 | logg.info("no more services - exit init-loop") 3979 | break 3980 | if self.exit_when_no_more_procs: 3981 | if not running: 3982 | logg.info("no more procs - exit init-loop") 3983 | break 3984 | except KeyboardInterrupt as e: 3985 | if e.args and e.args[0] == "SIGQUIT": 3986 | # the original systemd puts a coredump on that signal. 3987 | logg.info("SIGQUIT - switch to no more procs check") 3988 | self.exit_when_no_more_procs = True 3989 | continue 3990 | signal.signal(signal.SIGTERM, signal.SIG_DFL) 3991 | signal.signal(signal.SIGINT, signal.SIG_DFL) 3992 | logg.info("interrupted - exit init-loop") 3993 | result = e.message or "STOPPED" 3994 | self.read_log_files(units) 3995 | self.read_log_files(units) 3996 | self.stop_log_files(units) 3997 | logg.debug("done - init loop") 3998 | return result 3999 | def system_reap_zombies(self): 4000 | """ check to reap children """ 4001 | selfpid = os.getpid() 4002 | running = 0 4003 | for pid in os.listdir("/proc"): 4004 | try: pid = int(pid) 4005 | except: continue 4006 | if pid == selfpid: 4007 | continue 4008 | proc_status = "/proc/%s/status" % pid 4009 | if os.path.isfile(proc_status): 4010 | zombie = False 4011 | ppid = -1 4012 | try: 4013 | for line in open(proc_status): 4014 | m = re.match(r"State:\s*Z.*", line) 4015 | if m: zombie = True 4016 | m = re.match(r"PPid:\s*(\d+)", line) 4017 | if m: ppid = int(m.group(1)) 4018 | except IOError as e: 4019 | logg.warning("%s : %s", proc_status, e) 4020 | continue 4021 | if zombie and ppid == os.getpid(): 4022 | logg.info("reap zombie %s", pid) 4023 | try: os.waitpid(pid, os.WNOHANG) 4024 | except OSError as e: 4025 | logg.warning("reap zombie %s: %s", e.strerror) 4026 | if os.path.isfile(proc_status): 4027 | if pid > 1: 4028 | running += 1 4029 | return running # except PID 0 and PID 1 4030 | def pidlist_of(self, pid): 4031 | try: pid = int(pid) 4032 | except: return [] 4033 | pidlist = [ pid ] 4034 | pids = [ pid ] 4035 | for depth in xrange(ProcMaxDepth): 4036 | for pid in os.listdir("/proc"): 4037 | try: pid = int(pid) 4038 | except: continue 4039 | proc_status = "/proc/%s/status" % pid 4040 | if os.path.isfile(proc_status): 4041 | try: 4042 | for line in open(proc_status): 4043 | if line.startswith("PPid:"): 4044 | ppid = line[len("PPid:"):].strip() 4045 | try: ppid = int(ppid) 4046 | except: continue 4047 | if ppid in pidlist and pid not in pids: 4048 | pids += [ pid ] 4049 | except IOError as e: 4050 | logg.warning("%s : %s", proc_status, e) 4051 | continue 4052 | if len(pids) != len(pidlist): 4053 | pidlist = pids[:] 4054 | continue 4055 | return pids 4056 | def etc_hosts(self): 4057 | path = "/etc/hosts" 4058 | if self._root: 4059 | return os_path(self._root, path) 4060 | return path 4061 | def force_ipv4(self, *args): 4062 | """ only ipv4 localhost in /etc/hosts """ 4063 | logg.debug("checking /etc/hosts for '::1 localhost'") 4064 | lines = [] 4065 | for line in open(self.etc_hosts()): 4066 | if "::1" in line: 4067 | newline = re.sub("\\slocalhost\\s", " ", line) 4068 | if line != newline: 4069 | logg.info("/etc/hosts: '%s' => '%s'", line.rstrip(), newline.rstrip()) 4070 | line = newline 4071 | lines.append(line) 4072 | f = open(self.etc_hosts(), "w") 4073 | for line in lines: 4074 | f.write(line) 4075 | f.close() 4076 | def force_ipv6(self, *args): 4077 | """ only ipv4 localhost in /etc/hosts """ 4078 | logg.debug("checking /etc/hosts for '127.0.0.1 localhost'") 4079 | lines = [] 4080 | for line in open(self.etc_hosts()): 4081 | if "127.0.0.1" in line: 4082 | newline = re.sub("\\slocalhost\\s", " ", line) 4083 | if line != newline: 4084 | logg.info("/etc/hosts: '%s' => '%s'", line.rstrip(), newline.rstrip()) 4085 | line = newline 4086 | lines.append(line) 4087 | f = open(self.etc_hosts(), "w") 4088 | for line in lines: 4089 | f.write(line) 4090 | f.close() 4091 | def show_help(self, *args): 4092 | """[command] -- show this help 4093 | """ 4094 | lines = [] 4095 | okay = True 4096 | prog = os.path.basename(sys.argv[0]) 4097 | if not args: 4098 | argz = {} 4099 | for name in dir(self): 4100 | arg = None 4101 | if name.startswith("system_"): 4102 | arg = name[len("system_"):].replace("_","-") 4103 | if name.startswith("show_"): 4104 | arg = name[len("show_"):].replace("_","-") 4105 | if name.endswith("_of_unit"): 4106 | arg = name[:-len("_of_unit")].replace("_","-") 4107 | if name.endswith("_modules"): 4108 | arg = name[:-len("_modules")].replace("_","-") 4109 | if arg: 4110 | argz[arg] = name 4111 | lines.append("%s command [options]..." % prog) 4112 | lines.append("") 4113 | lines.append("Commands:") 4114 | for arg in sorted(argz): 4115 | name = argz[arg] 4116 | method = getattr(self, name) 4117 | doc = "..." 4118 | doctext = getattr(method, "__doc__") 4119 | if doctext: 4120 | doc = doctext 4121 | elif not self._show_all: 4122 | continue # pragma: nocover 4123 | firstline = doc.split("\n")[0] 4124 | doc_text = firstline.strip() 4125 | if "--" not in firstline: 4126 | doc_text = "-- " + doc_text 4127 | lines.append(" %s %s" % (arg, firstline.strip())) 4128 | return lines 4129 | for arg in args: 4130 | arg = arg.replace("-","_") 4131 | func1 = getattr(self.__class__, arg+"_modules", None) 4132 | func2 = getattr(self.__class__, arg+"_of_unit", None) 4133 | func3 = getattr(self.__class__, "show_"+arg, None) 4134 | func4 = getattr(self.__class__, "system_"+arg, None) 4135 | func = func1 or func2 or func3 or func4 4136 | if func is None: 4137 | print("error: no such command '%s'" % arg) 4138 | okay = False 4139 | else: 4140 | doc_text = "..." 4141 | doc = getattr(func, "__doc__", None) 4142 | if doc: 4143 | doc_text = doc.replace("\n","\n\n", 1).strip() 4144 | if "--" not in doc_text: 4145 | doc_text = "-- " + doc_text 4146 | else: 4147 | logg.debug("__doc__ of %s is none", func_name) 4148 | if not self._show_all: continue 4149 | lines.append("%s %s %s" % (prog, arg, doc_text)) 4150 | if not okay: 4151 | self.show_help() 4152 | return False 4153 | return lines 4154 | def systemd_version(self): 4155 | """ the version line for systemd compatibility """ 4156 | return "systemd %s\n - via systemctl.py %s" % (self._systemd_version, __version__) 4157 | def systemd_features(self): 4158 | """ the info line for systemd features """ 4159 | features1 = "-PAM -AUDIT -SELINUX -IMA -APPARMOR -SMACK" 4160 | features2 = " +SYSVINIT -UTMP -LIBCRYPTSETUP -GCRYPT -GNUTLS" 4161 | features3 = " -ACL -XZ -LZ4 -SECCOMP -BLKID -ELFUTILS -KMOD -IDN" 4162 | return features1+features2+features3 4163 | def systems_version(self): 4164 | return [ self.systemd_version(), self.systemd_features() ] 4165 | 4166 | def print_result(result): 4167 | # logg_info = logg.info 4168 | # logg_debug = logg.debug 4169 | def logg_info(*msg): pass 4170 | def logg_debug(*msg): pass 4171 | exitcode = 0 4172 | if result is None: 4173 | logg_info("EXEC END None") 4174 | elif result is True: 4175 | logg_info("EXEC END True") 4176 | result = None 4177 | exitcode = 0 4178 | elif result is False: 4179 | logg_info("EXEC END False") 4180 | result = None 4181 | exitcode = 1 4182 | elif isinstance(result, tuple) and len(result) == 2: 4183 | exitcode, status = result 4184 | logg_info("EXEC END %s '%s'", exitcode, status) 4185 | if exitcode is True: exitcode = 0 4186 | if exitcode is False: exitcode = 1 4187 | result = status 4188 | elif isinstance(result, int): 4189 | logg_info("EXEC END %s", result) 4190 | exitcode = result 4191 | result = None 4192 | # 4193 | if result is None: 4194 | pass 4195 | elif isinstance(result, string_types): 4196 | print(result) 4197 | result1 = result.split("\n")[0][:-20] 4198 | if result == result1: 4199 | logg_info("EXEC END '%s'", result) 4200 | else: 4201 | logg_info("EXEC END '%s...'", result1) 4202 | logg_debug(" END '%s'", result) 4203 | elif isinstance(result, list) or hasattr(result, "next") or hasattr(result, "__next__"): 4204 | shown = 0 4205 | for element in result: 4206 | if isinstance(element, tuple): 4207 | print("\t".join([ str(elem) for elem in element] )) 4208 | else: 4209 | print(element) 4210 | shown += 1 4211 | logg_info("EXEC END %s items", shown) 4212 | logg_debug(" END %s", result) 4213 | elif hasattr(result, "keys"): 4214 | shown = 0 4215 | for key in sorted(result.keys()): 4216 | element = result[key] 4217 | if isinstance(element, tuple): 4218 | print(key,"=","\t".join([ str(elem) for elem in element])) 4219 | else: 4220 | print("%s=%s" % (key,element)) 4221 | shown += 1 4222 | logg_info("EXEC END %s items", shown) 4223 | logg_debug(" END %s", result) 4224 | else: 4225 | logg.warning("EXEC END Unknown result type %s", str(type(result))) 4226 | return exitcode 4227 | 4228 | if __name__ == "__main__": 4229 | import optparse 4230 | _o = optparse.OptionParser("%prog [options] command [name...]", 4231 | epilog="use 'help' command for more information") 4232 | _o.add_option("--version", action="store_true", 4233 | help="Show package version") 4234 | _o.add_option("--system", action="store_true", default=False, 4235 | help="Connect to system manager (default)") # overrides --user 4236 | _o.add_option("--user", action="store_true", default=_user_mode, 4237 | help="Connect to user service manager") 4238 | # _o.add_option("-H", "--host", metavar="[USER@]HOST", 4239 | # help="Operate on remote host*") 4240 | # _o.add_option("-M", "--machine", metavar="CONTAINER", 4241 | # help="Operate on local container*") 4242 | _o.add_option("-t","--type", metavar="TYPE", dest="unit_type", default=_unit_type, 4243 | help="List units of a particual type") 4244 | _o.add_option("--state", metavar="STATE", default=_unit_state, 4245 | help="List units with particular LOAD or SUB or ACTIVE state") 4246 | _o.add_option("-p", "--property", metavar="NAME", dest="unit_property", default=_unit_property, 4247 | help="Show only properties by this name") 4248 | _o.add_option("-a", "--all", action="store_true", dest="show_all", default=_show_all, 4249 | help="Show all loaded units/properties, including dead empty ones. To list all units installed on the system, use the 'list-unit-files' command instead") 4250 | _o.add_option("-l","--full", action="store_true", default=_full, 4251 | help="Don't ellipsize unit names on output (never ellipsized)") 4252 | _o.add_option("--reverse", action="store_true", 4253 | help="Show reverse dependencies with 'list-dependencies' (ignored)") 4254 | _o.add_option("--job-mode", metavar="MODE", 4255 | help="Specifiy how to deal with already queued jobs, when queuing a new job (ignored)") 4256 | _o.add_option("--show-types", action="store_true", 4257 | help="When showing sockets, explicitly show their type (ignored)") 4258 | _o.add_option("-i","--ignore-inhibitors", action="store_true", 4259 | help="When shutting down or sleeping, ignore inhibitors (ignored)") 4260 | _o.add_option("--kill-who", metavar="WHO", 4261 | help="Who to send signal to (ignored)") 4262 | _o.add_option("-s", "--signal", metavar="SIG", 4263 | help="Which signal to send (ignored)") 4264 | _o.add_option("--now", action="store_true", default=_now, 4265 | help="Start or stop unit in addition to enabling or disabling it") 4266 | _o.add_option("-q","--quiet", action="store_true", default=_quiet, 4267 | help="Suppress output") 4268 | _o.add_option("--no-block", action="store_true", default=False, 4269 | help="Do not wait until operation finished (ignored)") 4270 | _o.add_option("--no-legend", action="store_true", default=_no_legend, 4271 | help="Do not print a legend (column headers and hints)") 4272 | _o.add_option("--no-wall", action="store_true", default=False, 4273 | help="Don't send wall message before halt/power-off/reboot (ignored)") 4274 | _o.add_option("--no-reload", action="store_true", 4275 | help="Don't reload daemon after en-/dis-abling unit files (ignored)") 4276 | _o.add_option("--no-ask-password", action="store_true", default=_no_ask_password, 4277 | help="Do not ask for system passwords") 4278 | # _o.add_option("--global", action="store_true", dest="globally", default=_globally, 4279 | # help="Enable/disable unit files globally") # for all user logins 4280 | # _o.add_option("--runtime", action="store_true", 4281 | # help="Enable unit files only temporarily until next reboot") 4282 | _o.add_option("--force", action="store_true", default=_force, 4283 | help="When enabling unit files, override existing symblinks / When shutting down, execute action immediately") 4284 | _o.add_option("--preset-mode", metavar="TYPE", default=_preset_mode, 4285 | help="Apply only enable, only disable, or all presets [%default]") 4286 | _o.add_option("--root", metavar="PATH", default=_root, 4287 | help="Enable unit files in the specified root directory (used for alternative root prefix)") 4288 | _o.add_option("-n","--lines", metavar="NUM", 4289 | help="Number of journal entries to show (ignored)") 4290 | _o.add_option("-o","--output", metavar="CAT", 4291 | help="change journal output mode [short, ..., cat] (ignored)") 4292 | _o.add_option("--plain", action="store_true", 4293 | help="Print unit dependencies as a list instead of a tree (ignored)") 4294 | _o.add_option("--no-pager", action="store_true", 4295 | help="Do not pipe output into pager (ignored)") 4296 | # 4297 | _o.add_option("--coverage", metavar="OPTIONLIST", default=COVERAGE, 4298 | help="..support for coverage (e.g. spawn,oldest,sleep) [%default]") 4299 | _o.add_option("-e","--extra-vars", "--environment", metavar="NAME=VAL", action="append", default=[], 4300 | help="..override settings in the syntax of 'Environment='") 4301 | _o.add_option("-v","--verbose", action="count", default=0, 4302 | help="..increase debugging information level") 4303 | _o.add_option("-4","--ipv4", action="store_true", default=False, 4304 | help="..only keep ipv4 localhost in /etc/hosts") 4305 | _o.add_option("-6","--ipv6", action="store_true", default=False, 4306 | help="..only keep ipv6 localhost in /etc/hosts") 4307 | _o.add_option("-1","--init", action="store_true", default=False, 4308 | help="..keep running as init-process (default if PID 1)") 4309 | opt, args = _o.parse_args() 4310 | logging.basicConfig(level = max(0, logging.FATAL - 10 * opt.verbose)) 4311 | logg.setLevel(max(0, logging.ERROR - 10 * opt.verbose)) 4312 | # 4313 | COVERAGE = opt.coverage 4314 | if "sleep" in COVERAGE: 4315 | MinimumTimeoutStartSec = 7 4316 | MinimumTimeoutStopSec = 7 4317 | if "quick" in COVERAGE: 4318 | MinimumTimeoutStartSec = 4 4319 | MinimumTimeoutStopSec = 4 4320 | DefaultTimeoutStartSec = 9 4321 | DefaultTimeoutStopSec = 9 4322 | _extra_vars = opt.extra_vars 4323 | _force = opt.force 4324 | _full = opt.full 4325 | _no_legend = opt.no_legend 4326 | _no_ask_password = opt.no_ask_password 4327 | _now = opt.now 4328 | _preset_mode = opt.preset_mode 4329 | _quiet = opt.quiet 4330 | _root = opt.root 4331 | _show_all = opt.show_all 4332 | _unit_state = opt.state 4333 | _unit_type = opt.unit_type 4334 | _unit_property = opt.unit_property 4335 | # being PID 1 (or 0) in a container will imply --init 4336 | _pid = os.getpid() 4337 | _init = opt.init or _pid in [ 1, 0 ] 4338 | _user_mode = opt.user 4339 | if os.geteuid() and _pid in [ 1, 0 ]: 4340 | _user_mode = True 4341 | if opt.system: 4342 | _user_mode = False # override --user 4343 | # 4344 | if _root: 4345 | _systemctl_debug_log = os_path(_root, _var(_systemctl_debug_log)) 4346 | _systemctl_extra_log = os_path(_root, _var(_systemctl_extra_log)) 4347 | elif _user_mode: 4348 | _systemctl_debug_log = _var(_systemctl_debug_log) 4349 | _systemctl_extra_log = _var(_systemctl_extra_log) 4350 | if os.access(_systemctl_extra_log, os.W_OK): 4351 | loggfile = logging.FileHandler(_systemctl_extra_log) 4352 | loggfile.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) 4353 | logg.addHandler(loggfile) 4354 | logg.setLevel(max(0, logging.INFO - 10 * opt.verbose)) 4355 | if os.access(_systemctl_debug_log, os.W_OK): 4356 | loggfile = logging.FileHandler(_systemctl_debug_log) 4357 | loggfile.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) 4358 | logg.addHandler(loggfile) 4359 | logg.setLevel(logging.DEBUG) 4360 | logg.info("EXEC BEGIN %s %s%s%s", os.path.realpath(sys.argv[0]), " ".join(args), 4361 | _user_mode and " --user" or " --system", _init and " --init" or "", ) 4362 | # 4363 | # 4364 | systemctl = Systemctl() 4365 | if opt.version: 4366 | args = [ "version" ] 4367 | if not args: 4368 | if _init: 4369 | args = [ "init" ] # alias "--init default" 4370 | else: 4371 | args = [ "list-units" ] 4372 | logg.debug("======= systemctl.py " + " ".join(args)) 4373 | command = args[0] 4374 | modules = args[1:] 4375 | if opt.ipv4: 4376 | systemctl.force_ipv4() 4377 | elif opt.ipv6: 4378 | systemctl.force_ipv6() 4379 | if _init: 4380 | time.sleep(EpsilonTime) 4381 | found = False 4382 | # command NAME 4383 | if command.startswith("__"): 4384 | command_name = command[2:] 4385 | command_func = getattr(systemctl, command_name, None) 4386 | if callable(command_func) and not found: 4387 | found = True 4388 | result = command_func(*modules) 4389 | command_name = command.replace("-","_").replace(".","_")+"_modules" 4390 | command_func = getattr(systemctl, command_name, None) 4391 | if callable(command_func) and not found: 4392 | found = True 4393 | result = command_func(*modules) 4394 | command_name = "show_"+command.replace("-","_").replace(".","_") 4395 | command_func = getattr(systemctl, command_name, None) 4396 | if callable(command_func) and not found: 4397 | found = True 4398 | result = command_func(*modules) 4399 | command_name = "system_"+command.replace("-","_").replace(".","_") 4400 | command_func = getattr(systemctl, command_name, None) 4401 | if callable(command_func) and not found: 4402 | found = True 4403 | result = command_func() 4404 | command_name = "systems_"+command.replace("-","_").replace(".","_") 4405 | command_func = getattr(systemctl, command_name, None) 4406 | if callable(command_func) and not found: 4407 | found = True 4408 | result = command_func() 4409 | if not found: 4410 | logg.error("Unknown operation %s.", command) 4411 | sys.exit(1) 4412 | # 4413 | sys.exit(print_result(result)) 4414 | --------------------------------------------------------------------------------