mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-09-19 04:59:01 +00:00
updates/improvements to exmachina
This commit is contained in:
parent
f1e764f2e5
commit
0430892518
@ -30,11 +30,12 @@ client in the same way. The init_test.sh script demonstrates this mechanism.
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import optparse
|
import grp
|
||||||
|
import shutil
|
||||||
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import stat
|
|
||||||
import time
|
import time
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
@ -43,9 +44,9 @@ import bjsonrpc.handlers
|
|||||||
import bjsonrpc.server
|
import bjsonrpc.server
|
||||||
import augeas
|
import augeas
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def execute_service(servicename, action, timeout=10):
|
def execute_service(servicename, action, timeout=10):
|
||||||
"""This function mostly ripped from StackOverflow:
|
"""This function mostly ripped from StackOverflow:
|
||||||
http://stackoverflow.com/questions/1556348/python-run-a-process-with-timeout-and-capture-stdout-stderr-and-exit-status
|
http://stackoverflow.com/questions/1556348/python-run-a-process-with-timeout-and-capture-stdout-stderr-and-exit-status
|
||||||
@ -54,7 +55,7 @@ def execute_service(servicename, action, timeout=10):
|
|||||||
script = "/etc/init.d/" + os.path.split(servicename)[1]
|
script = "/etc/init.d/" + os.path.split(servicename)[1]
|
||||||
|
|
||||||
if not os.path.exists(script):
|
if not os.path.exists(script):
|
||||||
return "ERROR: so such service"
|
raise ValueError("so such service: %s" % servicename)
|
||||||
|
|
||||||
command_list = [script, action]
|
command_list = [script, action]
|
||||||
log.info("executing: %s" % command_list)
|
log.info("executing: %s" % command_list)
|
||||||
@ -64,18 +65,51 @@ def execute_service(servicename, action, timeout=10):
|
|||||||
stderr=subprocess.PIPE)
|
stderr=subprocess.PIPE)
|
||||||
poll_seconds = .250
|
poll_seconds = .250
|
||||||
deadline = time.time() + timeout
|
deadline = time.time() + timeout
|
||||||
while time.time() < deadline and proc.poll() == None:
|
while time.time() < deadline and proc.poll() is None:
|
||||||
time.sleep(poll_seconds)
|
time.sleep(poll_seconds)
|
||||||
|
|
||||||
if proc.poll() == None:
|
if proc.poll() is None:
|
||||||
if float(sys.version[:3]) >= 2.6:
|
if float(sys.version[:3]) >= 2.6:
|
||||||
proc.terminate()
|
proc.terminate()
|
||||||
raise Exception("execution timed out (>%d seconds): %s" %
|
raise Exception("execution timed out (>%d seconds): %s" %
|
||||||
|
(timeout, command_list))
|
||||||
|
|
||||||
|
stdout, stderr = proc.communicate()
|
||||||
|
# TODO: should raise exception here if proc.returncode != 0?
|
||||||
|
return stdout, stderr, proc.returncode
|
||||||
|
|
||||||
|
def execute_apt(packagename, action, timeout=120, aptargs=['-q', '-y']):
|
||||||
|
# ensure package name isn't tricky trick
|
||||||
|
if action != "update" \
|
||||||
|
and (packagename != packagename.strip().split()[0] \
|
||||||
|
or packagename.startswith('-')):
|
||||||
|
raise ValueError("Not a good apt package name: %s" % packagename)
|
||||||
|
|
||||||
|
if action == "update":
|
||||||
|
command_list = ['apt-get', action]
|
||||||
|
else:
|
||||||
|
command_list = ['apt-get', action, packagename]
|
||||||
|
command_list.extend(aptargs)
|
||||||
|
log.info("executing: %s" % command_list)
|
||||||
|
proc = subprocess.Popen(command_list,
|
||||||
|
bufsize=0,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
poll_seconds = .250
|
||||||
|
deadline = time.time() + timeout
|
||||||
|
while time.time() < deadline and proc.poll() is None:
|
||||||
|
time.sleep(poll_seconds)
|
||||||
|
|
||||||
|
if proc.poll() is None:
|
||||||
|
if float(sys.version[:3]) >= 2.6:
|
||||||
|
proc.terminate()
|
||||||
|
raise Exception("execution timed out (>%d seconds): %s" %
|
||||||
(timeout, command_list))
|
(timeout, command_list))
|
||||||
|
|
||||||
stdout, stderr = proc.communicate()
|
stdout, stderr = proc.communicate()
|
||||||
return stdout, stderr, proc.returncode
|
return stdout, stderr, proc.returncode
|
||||||
|
|
||||||
|
|
||||||
class ExMachinaHandler(bjsonrpc.handlers.BaseHandler):
|
class ExMachinaHandler(bjsonrpc.handlers.BaseHandler):
|
||||||
|
|
||||||
# authentication state variable. If not None, still need to authenticate;
|
# authentication state variable. If not None, still need to authenticate;
|
||||||
@ -145,6 +179,24 @@ class ExMachinaHandler(bjsonrpc.handlers.BaseHandler):
|
|||||||
log.info("augeas: remove %s" % path)
|
log.info("augeas: remove %s" % path)
|
||||||
return self.augeas.remove(path.encode('utf-8'))
|
return self.augeas.remove(path.encode('utf-8'))
|
||||||
|
|
||||||
|
# ------------- Misc. non-Augeas Helpers -----------------
|
||||||
|
def set_timezone(self, tzname):
|
||||||
|
if not self.secret_key:
|
||||||
|
log.info("reset timezone to %s" % tzname)
|
||||||
|
tzname = tzname.strip()
|
||||||
|
tzpath = os.path.join("/usr/share/zoneinfo", tzname)
|
||||||
|
try:
|
||||||
|
os.stat(tzpath)
|
||||||
|
except OSError:
|
||||||
|
# file not found
|
||||||
|
raise ValueError("timezone not valid: %s" % tzname)
|
||||||
|
shutil.copy(
|
||||||
|
os.path.join("/usr/share/zoneinfo", tzname),
|
||||||
|
"/etc/localtime")
|
||||||
|
with open("/etc/timezone", "w") as tzfile:
|
||||||
|
tzfile.write(tzname + "\n")
|
||||||
|
return "timezone changed to %s" % tzname
|
||||||
|
|
||||||
# ------------- init.d Service Control -----------------
|
# ------------- init.d Service Control -----------------
|
||||||
def initd_status(self, servicename):
|
def initd_status(self, servicename):
|
||||||
if not self.secret_key:
|
if not self.secret_key:
|
||||||
@ -161,11 +213,26 @@ class ExMachinaHandler(bjsonrpc.handlers.BaseHandler):
|
|||||||
def initd_restart(self, servicename):
|
def initd_restart(self, servicename):
|
||||||
if not self.secret_key:
|
if not self.secret_key:
|
||||||
return execute_service(servicename, "restart")
|
return execute_service(servicename, "restart")
|
||||||
|
|
||||||
|
# ------------- apt-get Package Control -----------------
|
||||||
|
def apt_install(self, packagename):
|
||||||
|
if not self.secret_key:
|
||||||
|
return execute_apt(packagename, "install")
|
||||||
|
|
||||||
|
def apt_update(self):
|
||||||
|
if not self.secret_key:
|
||||||
|
return execute_apt("", "update")
|
||||||
|
|
||||||
|
def apt_remove(self, packagename):
|
||||||
|
if not self.secret_key:
|
||||||
|
return execute_apt(packagename, "remove")
|
||||||
|
|
||||||
|
|
||||||
class EmptyClass():
|
class EmptyClass():
|
||||||
# Used by ExMachinaClient below
|
# Used by ExMachinaClient below
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ExMachinaClient():
|
class ExMachinaClient():
|
||||||
"""Simple client wrapper library to expose augeas and init.d methods.
|
"""Simple client wrapper library to expose augeas and init.d methods.
|
||||||
|
|
||||||
@ -193,6 +260,8 @@ class ExMachinaClient():
|
|||||||
|
|
||||||
self.augeas = EmptyClass()
|
self.augeas = EmptyClass()
|
||||||
self.initd = EmptyClass()
|
self.initd = EmptyClass()
|
||||||
|
self.apt = EmptyClass()
|
||||||
|
self.misc = EmptyClass()
|
||||||
|
|
||||||
self.augeas.save = self.conn.call.augeas_save
|
self.augeas.save = self.conn.call.augeas_save
|
||||||
self.augeas.set = self.conn.call.augeas_set
|
self.augeas.set = self.conn.call.augeas_set
|
||||||
@ -206,11 +275,16 @@ class ExMachinaClient():
|
|||||||
self.initd.start = self.conn.call.initd_start
|
self.initd.start = self.conn.call.initd_start
|
||||||
self.initd.stop = self.conn.call.initd_stop
|
self.initd.stop = self.conn.call.initd_stop
|
||||||
self.initd.restart = self.conn.call.initd_restart
|
self.initd.restart = self.conn.call.initd_restart
|
||||||
|
self.apt.install = self.conn.call.apt_install
|
||||||
|
self.apt.update = self.conn.call.apt_update
|
||||||
|
self.apt.remove = self.conn.call.apt_remove
|
||||||
|
self.misc.set_timezone = self.conn.call.set_timezone
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.sock.close()
|
self.sock.close()
|
||||||
|
|
||||||
def run_server(socket_path, secret_key=None):
|
|
||||||
|
def run_server(socket_path, secret_key=None, socket_group=None):
|
||||||
|
|
||||||
if not 0 == os.geteuid():
|
if not 0 == os.geteuid():
|
||||||
log.warn("Expected to be running as root!")
|
log.warn("Expected to be running as root!")
|
||||||
@ -221,15 +295,20 @@ def run_server(socket_path, secret_key=None):
|
|||||||
sock.bind(socket_path)
|
sock.bind(socket_path)
|
||||||
sock.listen(1)
|
sock.listen(1)
|
||||||
|
|
||||||
# TODO: www-data group permissions only?
|
if socket_group is not None:
|
||||||
os.chmod(socket_path, 0666)
|
socket_uid = os.stat(socket_path).st_uid
|
||||||
|
socket_gid = grp.getgrnam(socket_group).gr_gid
|
||||||
|
os.chmod(socket_path, 0660)
|
||||||
|
os.chown(socket_path, socket_uid, socket_gid)
|
||||||
|
else:
|
||||||
|
os.chmod(socket_path, 0666)
|
||||||
if secret_key:
|
if secret_key:
|
||||||
ExMachinaHandler.secret_key = secret_key
|
ExMachinaHandler.secret_key = secret_key
|
||||||
|
|
||||||
serv = bjsonrpc.server.Server(sock, handler_factory=ExMachinaHandler)
|
serv = bjsonrpc.server.Server(sock, handler_factory=ExMachinaHandler)
|
||||||
serv.serve()
|
serv.serve()
|
||||||
|
|
||||||
def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
|
def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
|
||||||
"""
|
"""
|
||||||
From: http://www.noah.org/wiki/Daemonize_Python
|
From: http://www.noah.org/wiki/Daemonize_Python
|
||||||
|
|
||||||
@ -241,30 +320,30 @@ def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
|
|||||||
output may not appear in the order that you expect. """
|
output may not appear in the order that you expect. """
|
||||||
|
|
||||||
# Do first fork.
|
# Do first fork.
|
||||||
try:
|
try:
|
||||||
pid = os.fork()
|
pid = os.fork()
|
||||||
if pid > 0:
|
if pid > 0:
|
||||||
sys.exit(0) # Exit first parent.
|
sys.exit(0) # Exit first parent.
|
||||||
except OSError, e:
|
except OSError, e:
|
||||||
sys.stderr.write ("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror) )
|
sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Decouple from parent environment.
|
# Decouple from parent environment.
|
||||||
os.chdir("/")
|
os.chdir("/")
|
||||||
os.umask(0)
|
os.umask(0)
|
||||||
os.setsid()
|
os.setsid()
|
||||||
|
|
||||||
# Do second fork.
|
# Do second fork.
|
||||||
try:
|
try:
|
||||||
pid = os.fork()
|
pid = os.fork()
|
||||||
if pid > 0:
|
if pid > 0:
|
||||||
sys.exit(0) # Exit second parent.
|
sys.exit(0) # Exit second parent.
|
||||||
except OSError, e:
|
except OSError, e:
|
||||||
sys.stderr.write ("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror) )
|
sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Now I am a daemon!
|
# Now I am a daemon!
|
||||||
|
|
||||||
# Redirect standard file descriptors.
|
# Redirect standard file descriptors.
|
||||||
si = open(stdin, 'r')
|
si = open(stdin, 'r')
|
||||||
so = open(stdout, 'a+')
|
so = open(stdout, 'a+')
|
||||||
@ -279,41 +358,44 @@ def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
|
|||||||
def main():
|
def main():
|
||||||
|
|
||||||
global log
|
global log
|
||||||
parser = optparse.OptionParser(usage=
|
parser = argparse.ArgumentParser(usage=
|
||||||
"usage: %prog [options]\n"
|
"usage: %prog [options]\n"
|
||||||
"%prog --help for more info."
|
"%prog --help for more info."
|
||||||
)
|
)
|
||||||
parser.add_option("-v", "--verbose",
|
parser.add_argument("-v", "--verbose",
|
||||||
default=False,
|
default=False,
|
||||||
help="Show more debugging statements",
|
help="Show more debugging statements",
|
||||||
action="store_true")
|
action="store_true")
|
||||||
parser.add_option("-q", "--quiet",
|
parser.add_argument("-q", "--quiet",
|
||||||
default=False,
|
default=False,
|
||||||
help="Show fewer informational statements",
|
help="Show fewer informational statements",
|
||||||
action="store_true")
|
action="store_true")
|
||||||
parser.add_option("-k", "--key",
|
parser.add_argument("-k", "--key",
|
||||||
default=False,
|
default=False,
|
||||||
help="Wait for Secret Access Key on stdin before starting",
|
help="Wait for Secret Access Key on stdin before starting",
|
||||||
action="store_true")
|
action="store_true")
|
||||||
parser.add_option("--random-key",
|
parser.add_argument("--random-key",
|
||||||
default=False,
|
default=False,
|
||||||
help="Just dump a random base64 key and exit",
|
help="Just dump a random base64 key and exit",
|
||||||
action="store_true")
|
action="store_true")
|
||||||
parser.add_option("-s", "--socket-path",
|
parser.add_argument("-s", "--socket-path",
|
||||||
default="/tmp/exmachina.sock",
|
default="/tmp/exmachina.sock",
|
||||||
help="UNIX Domain socket file path to listen on",
|
help="UNIX Domain socket file path to listen on",
|
||||||
metavar="FILE")
|
metavar="FILE")
|
||||||
parser.add_option("--pidfile",
|
parser.add_argument("--pidfile",
|
||||||
default=None,
|
default=None,
|
||||||
help="Daemonize and write pid to this file",
|
help="Daemonize and write pid to this file",
|
||||||
metavar="FILE")
|
metavar="FILE")
|
||||||
|
parser.add_argument("-g", "--group",
|
||||||
|
default=None,
|
||||||
|
help="chgrp socket file to this group and set 0660 permissions")
|
||||||
|
|
||||||
(options, args) = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if len(args) != 0:
|
#if len(args) != 0:
|
||||||
parser.error("Incorrect number of arguments")
|
#parser.error("Incorrect number of arguments")
|
||||||
|
|
||||||
if options.random_key:
|
if args.random_key:
|
||||||
sys.stdout.write(base64.urlsafe_b64encode(os.urandom(128)))
|
sys.stdout.write(base64.urlsafe_b64encode(os.urandom(128)))
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
@ -323,31 +405,33 @@ def main():
|
|||||||
hdlr.setFormatter(formatter)
|
hdlr.setFormatter(formatter)
|
||||||
log.addHandler(hdlr)
|
log.addHandler(hdlr)
|
||||||
|
|
||||||
if options.verbose:
|
if args.verbose:
|
||||||
log.setLevel(logging.DEBUG)
|
log.setLevel(logging.DEBUG)
|
||||||
elif options.quiet:
|
elif args.quiet:
|
||||||
log.setLevel(logging.ERROR)
|
log.setLevel(logging.ERROR)
|
||||||
else:
|
else:
|
||||||
log.setLevel(logging.INFO)
|
log.setLevel(logging.INFO)
|
||||||
|
|
||||||
secret_key = None
|
secret_key = None
|
||||||
if options.key:
|
if args.key:
|
||||||
log.debug("Waiting for secret key on stdin...")
|
log.debug("Waiting for secret key on stdin...")
|
||||||
secret_key = sys.stdin.readline().strip()
|
secret_key = sys.stdin.readline().strip()
|
||||||
log.debug("Got it!")
|
log.debug("Got it!")
|
||||||
|
|
||||||
if options.pidfile:
|
if args.pidfile:
|
||||||
with open(options.pidfile, 'w') as pfile:
|
with open(args.pidfile, 'w') as pfile:
|
||||||
# ensure file is available/writable
|
# ensure file is available/writable
|
||||||
pass
|
pass
|
||||||
os.unlink(options.pidfile)
|
os.unlink(args.pidfile)
|
||||||
daemonize()
|
daemonize()
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
with open(options.pidfile, 'w') as pfile:
|
with open(args.pidfile, 'w') as pfile:
|
||||||
pfile.write("%s" % pid)
|
pfile.write("%s" % pid)
|
||||||
log.info("Daemonized, pid is %s" % pid)
|
log.info("Daemonized, pid is %s" % pid)
|
||||||
|
|
||||||
run_server(secret_key=secret_key, socket_path=options.socket_path)
|
run_server(secret_key=secret_key,
|
||||||
|
socket_path=args.socket_path,
|
||||||
|
socket_group=args.group)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
export key=`./exmachina.py --random-key`
|
export key=`./exmachina.py --random-key`
|
||||||
|
|
||||||
echo $key | ./exmachina.py -vk --pidfile /tmp/exmachina_test.pid
|
echo $key | ./exmachina.py -vk --pidfile /tmp/exmachina_test.pid -g www-data
|
||||||
sleep 1
|
sleep 1
|
||||||
echo $key | sudo -u www-data -g www-data ./test_exmachina.py -k
|
echo $key | sudo -u www-data -g www-data ./test_exmachina.py -k
|
||||||
|
|
||||||
|
|||||||
@ -20,13 +20,11 @@ at the same time:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import optparse
|
|
||||||
import logging
|
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
import bjsonrpc
|
import bjsonrpc
|
||||||
import bjsonrpc.connection
|
import bjsonrpc.connection
|
||||||
import augeas
|
from bjsonrpc.exceptions import ServerError
|
||||||
|
|
||||||
from exmachina import ExMachinaClient
|
from exmachina import ExMachinaClient
|
||||||
|
|
||||||
@ -34,7 +32,7 @@ from exmachina import ExMachinaClient
|
|||||||
# Command line handling
|
# Command line handling
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
socket_path="/tmp/exmachina.sock"
|
socket_path = "/tmp/exmachina.sock"
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
sock.connect(socket_path)
|
sock.connect(socket_path)
|
||||||
|
|
||||||
@ -62,9 +60,22 @@ def main():
|
|||||||
client = ExMachinaClient(secret_key=secret_key)
|
client = ExMachinaClient(secret_key=secret_key)
|
||||||
print client.augeas.match("/files/etc/*")
|
print client.augeas.match("/files/etc/*")
|
||||||
#print client.initd.restart("bluetooth")
|
#print client.initd.restart("bluetooth")
|
||||||
print client.initd.status("greentooth")
|
try:
|
||||||
|
print client.initd.status("greentooth")
|
||||||
|
print "ERROR: should have failed above!"
|
||||||
|
except ServerError:
|
||||||
|
print "(got expected error, good!)"
|
||||||
print "(expect Error on the above line)"
|
print "(expect Error on the above line)"
|
||||||
print client.initd.status("bluetooth")
|
print client.initd.status("bluetooth")
|
||||||
|
print client.apt.install("pkg_which_does_not_exist")
|
||||||
|
print client.apt.remove("pkg_which_does_not_exist")
|
||||||
|
#print client.apt.update() # can be slow...
|
||||||
|
#print client.misc.set_timezone("UTC") # don't clobber system...
|
||||||
|
try:
|
||||||
|
print client.misc.set_timezone("whoopie") # should be an error
|
||||||
|
print "ERROR: should have failed above!"
|
||||||
|
except ServerError:
|
||||||
|
print "(got expected error, good!)"
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user