I know there have been a lot of similar questions, but for the life of me I can't figure out what's wrong! I have a script that forwards a port via ssh tunnel, and it works pretty fine, and I want to run it as a service on startup using systemd. I already have other services running on my Pi as systemd services, and they work well, so I can't figure out why this one is not. I'm using Raspbian Jessie (server, no gui, headless Pi).
So, the unit file is as follows:
[Unit]
Description=SSH tunnel for DomoPro
After=multi-user.target sshd.service network.target
[Service]
Type=idle
ExecStart=/usr/bin/python /usr/bin/pyScripts/mkTunnel.py
ExecStop=/usr/bin/python /usr/bin/pyScripts/rmTunnel.py
StandardOutput=journal
[Install]
WantedBy=multi-user.target
and the python script, in case it's necessary, is as follows:
#!/usr/bin/python
import MySQLdb
from time import time,sleep
from subprocess import Popen,call,PIPE
import pycurl
from StringIO import StringIO
import json
from math import floor
print("DomoPro Tunnel Service")
def getSerial():
cursor.execute("SELECT value FROM config WHERE var='serialKey'")
cfg = cursor.fetchone()
return cfg['value']
def setPort(port):
sqlupdt = "UPDATE config SET value = '{0}' WHERE var='remotePort'".format(port)
cursor.execute(sqlupdt)
def savePID(tunnelPID):
sqlupdt = "UPDATE config SET value = '{0}' WHERE var='tunnelPID'".format(tunnelPID)
cursor.execute(sqlupdt)
def clearPID():
sqlupdt = "UPDATE config SET value = '' WHERE var='tunnelPID'"
cursor.execute(sqlupdt)
def getTunnelData(serialKey):
from urllib import urlencode
ans = StringIO()
serverURL = "server_address/phpfile.php"
data_post = urlencode({"serialKey": serialKey})
curl = pycurl.Curl()
curl.setopt(curl.URL, serverURL)
curl.setopt(curl.WRITEFUNCTION, ans.write)
curl.setopt(curl.USERAGENT, "Some custom useragent")
curl.setopt(curl.FAILONERROR, 1)
curl.setopt(curl.CONNECTTIMEOUT, 10)
curl.setopt(curl.HTTPHEADER,['Accept: application/json'])
curl.setopt(curl.POST, 1)
curl.setopt(curl.POSTFIELDS, data_post)
try:
curl.perform()
except pycurl.error:
return (False,False,False)
curl.close()
try:
data = json.loads(ans.getvalue())
except ValueError:
return False
else:
(auth,port,isActive) = data['auth'], data['port'], data['isActive']
if auth == "OK":
return (auth,port,isActive)
else:
return (auth,False,False)
def openTunnel(port):
if port:
tunnel = call(['ssh','-fNq','-o','ConnectTimeout=5','-o','BatchMode=yes','-o','StrictHostKeyChecking=no','-o','ExitOnForwardFailure=yes','-R','{0}:localhost:443'.format(port),'user@server'])
return tunnel
else:
return 1
def getpsPID():
objPID = Popen('ps -C "ssh -fNq" -o pid=', shell=True, stdout=PIPE, stderr=PIPE)
PID = objPID.communicate()[0].replace('\n','').replace(' ','')
return PID
def getdbPID():
cursor.execute("SELECT value FROM config WHERE var='tunnelPID'")
cfg = cursor.fetchone()
return cfg['value']
def makeTunnel():
'''
Return Codes:
0: Ok
1: Failed
2: Not Active
3: Auth Err
4: Conn Err
'''
print("Opening tunnel...")
serialKey = getSerial()
(auth,port,isActive) = getTunnelData(serialKey)
if auth == "OK":
if isActive:
setPort(port)
tunnel = openTunnel(port)
if not tunnel:
tunnelPID = getpsPID()
savePID(tunnelPID)
print("Tunnel open")
return 0
else:
clearPID()
print("Tunnel failed")
return 1
else:
setPort(0)
clearPID()
print("DomoPro Remote not active")
return 2
elif auth == "ERROR":
setPort(0)
clearPID()
print("Auth error")
return 3
else:
setPort(0)
clearPID()
print("Connection failed")
return 4
def checkTunnel():
print("Checking tunnel...")
dbPID = getdbPID()
psPID = getpsPID()
if psPID:
if dbPID != psPID:
savePID(psPID)
print("Tunnel open, PID:{}".format(psPID))
return 0
else:
clearPID()
print("Tunnel closed")
return 1
db = MySQLdb.connect( host="localhost",
user="user",
passwd="pass",
db="db")
db.autocommit(True)
cursor = db.cursor(MySQLdb.cursors.DictCursor)
tunnelState = checkTunnel()
if tunnelState:
tunnelState = makeTunnel()
ts = time()
while True:
currts = time()
if (tunnelState == 0 or tunnelState == 1):
timeoff = 120
if (currts - ts) > timeoff:
tunnelState = checkTunnel()
if tunnelState:
tunnelState = makeTunnel()
ts = time()
else:
diff = timeoff - floor(currts - ts)
print("Sleeping for {}s...".format(diff))
sleep(diff)
elif tunnelState == 2:
timeoff = 3600
if (currts - ts) > timeoff:
tunnelState = makeTunnel()
ts = time()
else:
diff = timeoff - floor(currts - ts)
print("Sleeping for {}s...".format(diff))
sleep(diff)
elif tunnelState == 3:
timeoff = 86400
if (currts - ts) > timeoff:
tunnelState = makeTunnel()
ts = time()
else:
diff = timeoff - floor(currts - ts)
print("Sleeping for {}s...".format(diff))
sleep(diff)
elif tunnelState == 4:
timeoff = 300
if (currts - ts) > timeoff:
tunnelState = makeTunnel()
ts = time()
else:
diff = timeoff - floor(currts - ts)
print("Sleeping for {}s...".format(diff))
sleep(diff)
cursor.close()
I also tried adding it to crontab using @reboot to no avail.
The script works flawlessly when run from command line.
You may notice there's another script running on service stop. It actually runs ok.
EDIT:
I followed Goldilocks' guide and wrapped the script like this:
#!/bin/bash
# Wrapper for mkTunnel.py
exec &>> /home/pi/log/tunnel.log
echo $(date)
# Fork/exec
(
exec /usr/bin/python /usr/bin/pyScripts/mkTunnel.py
) &>> /home/pi/log/tunnel.log
exit 0
And it runs ok, but since my python script keeps running in background, and I guess that systemd is expecting an exit code, it shows an error message Job for tunnel.service failed. The journal shows the following:
Starting SSH tunnel for DomoPro...
tunnel.service start operation timed out. Terminating.
Failed to start SSH tunnel for DomoPro.
Unit tunnel.service entered failed state.
My current unit file is this:
[Unit]
Description=SSH tunnel for DomoPro
After=multi-user.target sshd.service network.target
[Service]
Type=forking
GuessMainPID=no
ExecStart=/usr/bin/pyScripts/tunnel.sh
ExecStop=/usr/bin/python /usr/bin/pyScripts/rmTunnel.py
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
How can I tell systemd not to timeout (or not to expect an exit code)?
EDIT 2:
It's working!! I forgot to remove verbosity from ssh and that was causing it to remain open, not returning an exit status for systemd. Thanks @goldilocks ;)