Montag, 16. Januar 2017

Dienst unter Raspbian einrichten

Wahrscheinlich passt das Thema unter jedem debian System, ich hab es aber auf dem Raspberry unter Jessie getestet.
Mein Ziel war es, selbst einen Dienst einzurichten und dabei die Unterschiede zwischen wheezy (SysVinit) und Jessie (SystemD) zu lernen.
Eine gute Beschreibung für meinen Start habe ich hier gefunden. Python ist wie Perl als Grundlage im Debian System schon vorhanden, man braucht also keine weitere Installation. Das Python Script aus dem genannten Artikel ist eine gute Grundlage und ich habe es etwas modifiziert.
Alle Scripte müssen im Unix Format (nur lf als Zeilenwechsel) gespeichert werden!

Das Demo-Script

Es schreibt kontinuierlich in eine "Tages"-Logdatei im /tmp Verzeichnis und kann bei normalen Start entweder mit ctrl-c oder im Hintergrund einfach mit kill beendet werden. Das Script demonstriert ein paar nützliche Techniken, ich habe lediglich den Abbruch am Ende entfernt und das Intervall auf eine Minute heraufgesetzt. So läuft das Script "ewig". Im Script selbst ist das Wichtigste dokumentiert.

#!/usr/bin/env python

import logging
import logging.handlers
import argparse
import sys
import time  # this is only being used as part of the example

# Defaults
LOG_FILENAME = "/tmp/myservice.log"
LOG_LEVEL = logging.INFO  # Could be e.g. "DEBUG" or "WARNING"

# Define and parse command line arguments
parser = argparse.ArgumentParser(description="My simple Python service")
parser.add_argument("-l", "--log", help="file to write log to (default '" + LOG_FILENAME + "')")

# If the log file is specified on the command line then override the default
args = parser.parse_args()
if args.log:
        LOG_FILENAME = args.log

# Configure logging to log to a file, making a new file at midnight and keeping the last 3 day's data
# Give the logger a unique name (good practice)
logger = logging.getLogger(__name__)
# Set the log level to LOG_LEVEL
logger.setLevel(LOG_LEVEL)
# Make a handler that writes to a file, making a new file at midnight and keeping 3 backups
handler = logging.handlers.TimedRotatingFileHandler(LOG_FILENAME, when="midnight", backupCount=3)
# Format each log message like this
formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')
# Attach the formatter to the handler
handler.setFormatter(formatter)
# Attach the handler to the logger
logger.addHandler(handler)

# Make a class we can use to capture stdout and sterr in the log
class MyLogger(object):
        def __init__(self, logger, level):
                """Needs a logger and a logger level."""
                self.logger = logger
                self.level = level

        def write(self, message):
                # Only log if there is a message (not just a new line)
                if message.rstrip() != "":
                        self.logger.log(self.level, message.rstrip())

# Replace stdout with logging to file at INFO level
sys.stdout = MyLogger(logger, logging.INFO)
# Replace stderr with logging to file at ERROR level
sys.stderr = MyLogger(logger, logging.ERROR)

i = 0

# Loop forever, doing something useful hopefully:
while True:
        logger.info("The counter is now " + str(i))
        print "This is a print"
        i += 1
        time.sleep(60)

SysVinit konfigurieren 

Init-Script

Ich möchte den Dienst unter einem normalen User laufen lassen und nicht als root (default). Im Pfad /etc/init.d/ befinden sich die Scripte welche zur Dienste Steuerung verwendet werden. Dort gibt es auch ein Script Namens skeleton, welches als Template verwendet werden kann. Also zunächst mal das Steuerungs Script, wiederum aus dem eingangs erwähnten Artikel, leicht modifiziert. Wie zu sehen ist, soll der Dienst unter dem User pi ($DAEMON_USER) und im Homedirectory ($DIR) vom User laufen.

#!/bin/sh

### BEGIN INIT INFO
# Provides:          myservice
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Put a short description of the service here
# Description:       Put a long description of the service here
### END INIT INFO

# Change the next 3 lines to suit where you install your script and what you want to call it
DIR=/home/pi
DAEMON=$DIR/myservice.py
DAEMON_NAME=myservice

# Add any command line options for your daemon here
DAEMON_OPTS=""

# This next line determines what user the script runs as.
# Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python.
DAEMON_USER=pi

# The process ID of the script when it runs is stored here:
PIDFILE=/var/run/$DAEMON_NAME.pid

. /lib/lsb/init-functions

do_start () {
    log_daemon_msg "Starting system $DAEMON_NAME daemon"
    start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS
    log_end_msg $?
}
do_stop () {
    log_daemon_msg "Stopping system $DAEMON_NAME daemon"
    start-stop-daemon --stop --pidfile $PIDFILE --retry 10
    log_end_msg $?
}

case "$1" in

    start|stop)
        do_${1}
        ;;

    restart|reload|force-reload)
        do_stop
        do_start
        ;;

    status)
        status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $?
        ;;

    *)
        echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}"
        exit 1
        ;;

esac
exit 0

Scripte kopieren und einrichten

Mit winscp werden die Scripte ins Home Verzeichnis von pi kopiert. Dann mit putty ein ssh Terminal öffnen und das Init-Script an Ort und Stelle kopieren, Rechte setzen und den Dienst einrichten.
sudo cp myservice.sh /etc/init.d/
chmod +x myservice.py
chmod +x /etc/init.d/myservice.sh
sudo update-rc.d myservice.sh defaults
Jetzt kann man mit den üblichen Befehlen oder durch direkten Script Aufruf den Dienst starten, abfragen und beenden.
sudo /etc/init.d/myservice.sh start
service myservice status
sudo service myservice stop
Will man den Dienst wieder entfernen, hilft uns update-rc.d mit den Optionen disable und remove.

SystemD

SystemD wird unter jessie mit installiert und läuft kompatibel und transparent. Man kann also sofort den Dienst auch mit systemctl <start|status|stop> myservice kontrollieren. Ich will aber nun die "alte" Einrichtung deaktivieren und den Dienst unter SystemD einrichten.
Ein Dienst in SystemD wird mit sogenannten Unit Files eingerichtet.

1. Variante: einfach das init.d Script verwenden

Im FHEM Forum gibt es ein HowTo wie man den existierenden Service in einen SystemD Service umwandelt. Die folgende Datei wird als myservice.service gespeichert und mit winscp wieder nach /home/pi übertragen.
Wie man sieht, wird dabei gleich das alte SysVinit Script recycelt.

[Unit]
Description=Test myservice

[Service]
Type=forking
ExecStart=/etc/init.d/myservice.sh start
ExecStop=/etc/init.d/myservice.sh stop

[Install]
WantedBy=multi-user.target

Zunächst also den Dienst wieder entfernen, die Datei nach /etc/systemd/system kopieren und systemd über die Änderung informieren.
sudo update-rc.d myservice.sh disable
sudo cp myservice.service /etc/systemd/system/
sudo systemctl --system daemon-reload
Jetzt kann man zur Probe den Dienst mit systemctl <start|status|stop> myservice starten. Wenn alles funktioniert muss er noch aktiviert werden.
sudo systemctl enable myservice

2. Variante: extra Startscript verwenden 

Hierauf basierend habe ich mir ein universelles Startscript erstellt. Zumindest mit meinem Pythonscript funktioniert das einwandfrei. Im Endeffekt ist es aber in der Funktion dem SysVinit adäquat. Die Funktion ist anders realisiert, ich finde es besser lesbar und der SysVinit Abschnitt am Anfang fehlt. Letztendlich könnte man sogar den Scriptaufruf als Parameter übergeben.
#!/bin/bash

PID=""
script="python myservice.py"

function get_pid {
   PID=`pidof $script`
}

function stop {
   get_pid
   if [ -z $PID ]; then
      echo "server is not running."
      exit 1
   else
      echo -n "Stopping server.."
      kill -9 $PID
      sleep 1
      echo ".. Done."
   fi
}


function start {
   get_pid
   if [ -z $PID ]; then
      echo  "Starting server.."
      $script &
      get_pid
      echo "Done. PID=$PID"
   else
      echo "server is already running, PID=$PID"
   fi
}

function restart {
   echo  "Restarting server.."
   get_pid
   if [ -z $PID ]; then
      start
   else
      stop
      sleep 5
      start
   fi
}


function status {
   get_pid
   if [ -z  $PID ]; then
      echo "Server is not running."
      exit 1
   else
      echo "Server is running, PID=$PID"
   fi
}

case "$1" in
   start)
      start
   ;;
   stop)
      stop
   ;;
   restart)
      restart
   ;;
   status)
      status
   ;;
   *)
      echo "Usage: $0 {start|stop|restart|status}"
esac

Praktisch gesehen ist der Unit Teil bei Systemd vom Startscript getrennt. Basierend auf dieser Dokumentation  habe ich  mal eine ziemlich "aufwändige" Variante erstellt. Hierbei wird start|stop|restart durch das obige Script gesteuert.

[Unit]
Description=Test myservice
After=network.target

[Service]
Type=forking
User=pi
WorkingDirectory=/home/pi
ExecStart=/home/pi/server.sh start
ExecStop=/home/pi/server.sh stop
ExecReload=/home/pi/server.sh restart
KillMode=none

[Install]
WantedBy=multi-user.target

3. Variante: Kurz und knapp einfach ein Unitfile erstellt

Hier übernimmt systemd die komplette Steuerung. Überraschenderweise funktioniert dies auch mit meinem Endlosscript. Ich habe die Anregung dazu von hier. Dort ist noch ein besseres Beispiel mit einem kleinem Webserver als Testscript dargestellt. Lässt man den User= Eintrag weg startet der Dienst als root.

[Unit]
Description=Test systemd myservice

[Service]
User=pi
ExecStart=/home/pi/myservice.py
StandardOutput=null

[Install]
WantedBy=multi-user.target

Keine Kommentare:

Kommentar veröffentlichen