Linux 應用程序開發入門

http://netkiller.github.io/journal/daemon.html

Mr. Neo Chen (陳景峯), netkiller, BG7NYT


中國廣東省深圳市龍華新區民治街道溪山美地
518131
+86 13113668890


版權聲明

轉載請與作者聯繫,轉載時請務必標明文章原始出處和作者信息及本聲明。

文檔出處:
http://netkiller.github.io
http://netkiller.sourceforge.net

微信掃瞄二維碼進入 Netkiller 微信訂閲號

QQ群:128659835 請註明“讀者”

2017-06-16: 2012-02-01 14:03:53 +0800 (Wed, 01 Feb 2012)

摘要

我會實現一個守護進程,從這個程序你將瞭解,Linux 應用程序開發基本流程

我們將實現一個遠程shell的功能,可以通過tcp協議,運行遠程機器上的命令或shell腳本

通過這個命令可以實現批量操作,管理上千台伺服器。需要發揮你的想象力,靈活使用它。

寫這個腳本,我是為了替代SSH遠程操作,因為SSH不能控制運行命令,操作風險大,也不安全。

程序還不完善,還需要很多後續改進工作,比如通過SSL建立Socket連結,用戶認證,ACL訪問控制等等.


目錄

1. 環境

OS: Ubuntu 11.10

Python: 3.2.2

程序目錄: /srv/nodekeeper

目錄與相關檔案

$ cd /srv
$ find nodekeeper | grep -v .svn
nodekeeper
nodekeeper/nodekeeper.ubuntu
nodekeeper/nodekeeper.cenos
nodekeeper/etc
nodekeeper/etc/commands.cfg
nodekeeper/etc/protocol.cfg
nodekeeper/bin
nodekeeper/bin/nodekeeper
nodekeeper/bin/console
		

2. nodekeeper 主程序

		
$ cat nodekeeper/bin/nodekeeper
#!/usr/bin/env python3
#/bin/env python3
#-*- coding: utf-8 -*-
##############################################
# Home  : http://netkiller.sf.net
# Author: Neo <openunix@163.com>
##############################################

import asyncore, asynchat, socket, threading
import subprocess, os, sys, getopt, configparser, logging
import string, re
from multiprocessing import Process

class Backend(asyncore.dispatcher):
    queue = []
    def __init__(self, host, port,config):
        asyncore.dispatcher.__init__(self)

        self.host = host
        self.port = port
        self.config = config

        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.bind((host,port))
        self.listen(10)

        try:
            cfg = Protocol(config['protocol'], self.host)
            #self.protocols = cfg.items(self.host)
            self.protocols = cfg.all()
            self.sections = cfg.sections()
        except configparser.NoSectionError as err:
            print("Error: %s %s" %(err, config['protocol']))
            sys.exit(2)
        try:
            logging.basicConfig(level=logging.NOTSET,
                    format='%(asctime)s %(levelname)-8s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S',
                    filename=config['logfile'],
                    filemode='a')
            self.logging = logging.getLogger()
            #self.logging.debug('Test')
        except AttributeError as err:
            print("Error: %s %s" %(err, config['logfile']))
            sys.exit(2)

    def handle_accept (self):
        conn, addr = self.accept()
        self.queue.append(addr)
        request_handler(conn, self)
    def handle_connect(self):
        pass
    def handle_expt(self):
        self.close()
    def handle_close(self):
        self.close()

class request_handler(asynchat.async_chat):

    def __init__(self, sock, resource):
        asynchat.async_chat.__init__(self, sock=sock)
        self.sessions = resource
        self.buffer = b''
        self.set_terminator(b"\r\n")
        self.logging 	= resource.logging
        self.protocols 	= resource.protocols
        self.sections 	= resource.sections
        self.host 	= self.sessions.host

    def handle_connect(self):
        # connection succeeded
        #self.logging.info('')
        pass

    def handle_expt(self):
        # connection failed
        self.close()

    def collect_incoming_data(self, data):
        """Buffer the data"""
        #self.buffer.append(data)
        self.buffer = data

    def found_terminator(self):
        try:
            buffer = bytes.decode(self.buffer)
        except UnicodeDecodeError:
            print("\r\nError: ",err)
            buffer = ''
        try:
            execute = re.split(' ', buffer)
            command = execute[0]
            parameter = ' '.join( execute[1:])
            response = b''
            screen = ''
            if self.buffer == b'quit' or self.buffer == b'exit' :
                self.push(b'shutdown!!!\r\n')
                self.close_when_done()
            elif self.buffer == b'help' or self.buffer == b'?':
                screen = "Help may be requested at any point in a command by entering a question mark '?' or 'help'. the help list will be showing the available options.\r\n"
            
                for cmd,v in self.protocols :
                    screen += cmd + "\r\n"
            elif self.buffer == b'sections' :
                for sect in self.sections :
                    screen += sect + "\r\n"
            elif self.buffer == b'help.html' :
                for cmd,v in self.protocols :
                    screen += '<a href="?host='+self.host+'&cmd='+cmd+'">'+ cmd +'</a><br />' + "\r\n"
            elif self.buffer == b'enable':
                self.prompt = b'#'
            elif self.buffer == b'end' or self.buffer == b'^z':
                self.prompt = b'>'
            else:
                proto = dict(self.protocols)
                if command in proto :
                    run = proto[command] + ' ' + parameter
                    screen = subprocess.getoutput(run)
    
            if screen :
                response = bytes(screen + "\r\n",'utf8')
                self.push(response)
                self.logging.info(bytes.decode(self.buffer))
            self.buffer = b''
            self.close_when_done()
        except :
            self.close_when_done()
            sys.exit(2)

class Protocol():
    config = None
    agreement = None
    def __init__(self,cfg = 'protocol.cfg',sections = ''):
        self.config = configparser.SafeConfigParser()
        self.config.read(cfg)
        #self.agreement = self.config.items('common')
    def sections(self):
        return self.config.sections()
    def items(self, sections):
        self.agreement = self.config.items(sections)
        return self.agreement
    def dicts(self):
        return dict(self.agreement)
    def all(self):
        self.agreement = []
        for section in self.config.sections():
            self.agreement += self.config.items(section)
        return self.agreement


def main():
    daemon = False
    host = 'localhost'
    port = 7800
    pidfile = ''
    logfile = ''
    cfgfile = ''

    try:
        opts, args = getopt.getopt(sys.argv[1:], "h:p:d?v", [ "daemon","host=","port=", 'help',"h=","p=", "basedir=", "pidfile=", "config=", "protocol=", "logfile="])

        if not opts :
            usage()
            sys.exit()
        for o, a in opts :
            if o in ('-?', '--help') :
                usage()
                sys.exit()
            elif o in ("-v", "--verbose"):
                usage()
                sys.exit()
            elif o in ("-d", "--daemon"):
                daemon = True
            elif o in ("-h", "--host"):
                host = a
            elif o in ("-p", "--port"):
                port = int(a)
            elif o in ("--basedir"):
                BASEDIR = a
            elif o in ("--pidfile"):
                pidfile = a
            elif o in ("--config"):
                cfgfile = a
            elif o in ("--protocol"):
                protocol = a		
            elif o in ("--logfile"):
                logfile = a
            else:
                assert False, "unhandled option"
    except getopt.GetoptError as err:
        # print help information and exit:
        usage()
        sys.exit(2)

    try:

        if daemon :
            pid = os.fork()
            if pid > 0:
                #exit first parent
                sys.exit(0)
        myself = str(sys.argv[0].split('/')[-1:][0])
        #pidfile = os.getpid()
        if not pidfile :
            pidfile = '/var/run/'+myself+'.pid'
        file = open(pidfile,'w')
        file.write(str(os.getpid()))
        file.close()
        if not cfgfile :
            cfgfile = ''+myself+'.cfg'
        if not logfile :
            logfile = '/var/log/'+myself+'.log'
        config = dict({'cfgfile':cfgfile, 'pidfile':pidfile, 'logfile':logfile, 'protocol':protocol})

        Backend(host,port,config)
        asyncore.loop(timeout=30, use_poll=True)

    except socket.error as err:
        print("\r\nError: ",err)
        sys.exit(2)
    except IOError as err:
        print("\r\nError: ",err)
        sys.exit(2)

def usage():
    myself = str(sys.argv[0].split('/')[-1:][0])

    print("Usage: %s -d -h <ip address> -p <7800>" % myself );
    print("Development and deployment administration platform")
    print("\r\nMandatory arguments to long options are mandatory for short options too.")
    print("\t-?, --help")
    print("\t-v, --verbose")
    print("\t-d, --daemon")
    print("\t-h, --host \t\t(default localhost)")
    print("\t-p, --port")
    print("\t    --config \t\t(default %s.cfg)" % myself)
    print("\t    --protocol \t\t(default %s.cfg)" % "protocol.cfg")
    print("\t    --pidfile \t\t(default /var/run/%s.pid)" % myself)
    print("\t    --logfile \t\t(default /var/log/%s.log)" % myself)
    print("\r\nExample:")
    print("\t%s --daemon --host localhost --port 7800" % myself)
    print("\t%s -d -h localhost -p 7800" % myself)
    print("\r\nSee http://netkiller.sf.net/ for updates, bug reports, and answers, \r\nif you have no web access, by sending email to Neo Chan<openunix@163.com>. ")
    # Exit status is 0 if OK, 1 if minor problems, 2 if serious trouble.

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print ("Crtl+C Pressed. Shutting down.")

		
		

2.1. 幫助信息

usage()

			
Usage: nodekeeper -d -h <ip address> -p <7800>
Development and deployment administration platform

Mandatory arguments to long options are mandatory for short options too.
	-?, --help
	-v, --verbose
	-d, --daemon
	-h, --host 		(default localhost)
	-p, --port
	    --config 		(default nodekeeper.cfg)
	    --protocol 		(default protocol.cfg.cfg)
	    --pidfile 		(default /var/run/nodekeeper.pid)
	    --logfile 		(default /var/log/nodekeeper.log)

Example:
	nodekeeper --daemon --host localhost --port 7800
	nodekeeper -d -h localhost -p 7800

See http://netkiller.sf.net/ for updates, bug reports, and answers, 
if you have no web access, by sending email to Neo Chan<openunix@163.com>.
			
			

2.2. 參數處理

getopt.getopt 實現Unix風格的命令參數,例如:

nodekeeper --daemon --host localhost --port 7800

--host localhost --port 7800 IP地址與連接埠參數
--daemon 參數實現後台運行
			

具體實現代碼

			
    try:
        opts, args = getopt.getopt(sys.argv[1:], "h:p:d?v", [ "daemon","host=","port=", 'help',"h=","p=", "basedir=", "pidfile=", "config=", "protocol=", "logfile="])

        if not opts :
            usage()
            sys.exit()
        for o, a in opts :
            if o in ('-?', '--help') :
                usage()
                sys.exit()
            elif o in ("-v", "--verbose"):
                usage()
                sys.exit()
            elif o in ("-d", "--daemon"):
                daemon = True
            elif o in ("-h", "--host"):
                host = a
            elif o in ("-p", "--port"):
                port = int(a)
            elif o in ("--basedir"):
                BASEDIR = a
            elif o in ("--pidfile"):
                pidfile = a
            elif o in ("--config"):
                cfgfile = a
            elif o in ("--protocol"):
                protocol = a		
            elif o in ("--logfile"):
                logfile = a
            else:
                assert False, "unhandled option"
    except getopt.GetoptError as err:
        # print help information and exit:
        usage()
        sys.exit(2)
			
			

2.3. 後台運行

--daemon 參數實現後台運行,原理是首先通過os.fork()克隆一個進程,然後退出當前進程,克隆的新進程繼續運行

如果是Shell程序,你可使用“&”符號後台運行,但作為一個應用程序,使用“&”顯得不專業。

具體實現的代碼如下

			
        if daemon :
            pid = os.fork()
            if pid > 0:
                #exit first parent
                sys.exit(0)
			
			

程序一旦進入後台,當前進程即將關閉,所以你必須保存PID,為後面的推出程序操作使用,這裡我們可以通過 --pidfile 指定一個pid檔案

2.4. 日誌記錄

程序一旦進入後台,你只能通過ps,pstree, top 等命令查看狀態,運行情況必須通過日誌的形式,打印出來

具體實現代碼如下:

			
			logging.basicConfig(level=logging.NOTSET,
                    format='%(asctime)s %(levelname)-8s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S',
                    filename=config['logfile'],
                    filemode='a')
            self.logging = logging.getLogger()
            self.logging.debug('Test')
			
			

2.5. 多綫程

繼承 asynchat.async_chat 實現多綫程

			
class request_handler(asynchat.async_chat):

    def __init__(self, sock, resource):
        asynchat.async_chat.__init__(self, sock=sock)
			
			

連接數限制

        self.listen(10)			
			

可以將這個參數提出來,然後通過命令行設置。

nodekeeper --daemon --maxconn 100 --host localhost --port 7800
			
self.max_connect = maxconn

self.listen(self.max_connect)			
			

3. 配置檔案

		
$ cat nodekeeper/etc/protocol.cfg 
[system]
ls = ls
os.hosts = cat /etc/hosts
os.issue = cat /etc/issue
os.memory = free
os.who = who
os.harddisk = df -h
os.uptime = uptime
os.cpuinfo = cat /proc/cpuinfo
os.meminfo = cat /proc/meminfo
os.dmesg = dmesg
os.process = ps aux
os.summary = echo
network.status = netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
network.netstat = netstat -nlp
network.ifconfig = ifconfig
network.route = ip route

[apache]
apache.start = /usr/local/apache/bin/apachectl start
apache.stop = /usr/local/apache/bin/apachectl stop
apache.restart = /usr/local/apache/bin/apachectl restart
apache.status = ps ax |grep httpd
apache.conf = cat /usr/local/apache/conf/httpd.conf
apache.conf.vhost = cat /usr/local/apache/conf/extra/httpd-vhosts.conf
apache.logs.now = 
apache.logs.tail = 

[resin]
resin.start = /usr/local/resin/bin/httpd.sh start
resin.stop = /usr/local/resin/bin/httpd.sh stop
resin.restart = /usr/local/resin/bin/httpd.sh restart
resin.status = /usr/local/resin/bin/httpd.sh status
resin.conf = cat /usr/local/resin/conf/resin.conf

[www]
www.list = ls -1 /www
www.permission = find /www -type d -exec chmod 755 {} \; find /www -type f -exec chmod 644 {} \; 
www.permission.777 = chmod 777 -R /www/*
lamp.status = ps ax |grep -E "mysqld|httpd|resin"

[samba]
samba.start = /etc/init.d/smb start
samba.stop = /etc/init.d/smb stop
samba.restart = /etc/init.d/smb restart
samba.status = /etc/init.d/smb status

[mysql]
mysql.start = /etc/init.d/mysql start
mysql.stop = /etc/init.d/mysql stop
mysql.restart = /etc/init.d/mysql restart

[memcache]
memcache.start = /etc/init.d/memcache start
memcache.stop = /etc/init.d/memcache stop
memcache.restart = /etc/init.d/memcache restart

[vsftpd]
vsftpd.start = /etc/init.d/vsftpd start
vsftpd.stop = /etc/init.d/vsftpd stop
vsftpd.restart = /etc/init.d/vsftpd restart
vsftpd.status = /etc/init.d/vsftpd status

		
		

4. init.d 腳本

Linux 所有守護進程都是用init.d下面的腳本來管理

當人你也可以直接運行命令:

nodekeeper --daemon --host localhost --port 7800
		

但這樣只能算是一個半成品,也不夠專業,我們寫的是linux運用程序,必須遵循Linux規範,所有要實現一個init.d腳本

		
$ cat nodekeeper
#! /bin/sh

### BEGIN INIT INFO
# Provides:          nodekeeper
# Required-Start:    $local_fs $remote_fs $network $syslog
# Required-Stop:     $local_fs $remote_fs $network $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the nodekeeper web server
# Description:       starts nodekeeper using start-stop-daemon
### END INIT INFO

PATH=/srv/nodekeeper/bin:$PATH
DAEMON=/srv/nodekeeper/bin/nodekeeper
NAME=nodekeeper
DESC=nodekeeper
BASEDIR="/srv/nodekeeper"
HOST=$(ifconfig  | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f2 | awk '{ print $1}'|head -n 1)
PORT=7800
CONFIG=$BASEDIR/etc/$NAME.cfg
LOGFILE=$BASEDIR/log/$NAME.log
PIDFILE=$BASEDIR/run/$NAME.pid
PIDFILE=/var/run/$NAME.pid
PROTOCOL=$BASEDIR/etc/protocol.cfg

DAEMON_OPTS="--daemon --host $HOST --port $PORT --config=$CONFIG --protocol=$PROTOCOL --pidfile=$PIDFILE --logfile=$LOGFILE"

test -x $DAEMON || exit 0

# Include nodekeeper defaults if available
if [ -f /etc/default/nodekeeper ] ; then
	. /etc/default/nodekeeper
fi

set -e

. /lib/lsb/init-functions

#test_nodekeeper_config() {
#  if $DAEMON -t $DAEMON_OPTS >/dev/null 2>&1
#  then
#    return 0
#  else
#    $DAEMON -t $DAEMON_OPTS
#    return $?
#  fi
#}

case "$1" in
  start)
	echo -n "Starting $DESC: "
	start-stop-daemon --start --quiet --pidfile /var/run/$NAME.pid \
		--exec $DAEMON -- $DAEMON_OPTS || true
	echo "$NAME."
	;;
  stop)
	echo -n "Stopping $DESC: "
	start-stop-daemon --stop --quiet --pidfile /var/run/$NAME.pid \
		--exec $DAEMON || true
	echo "$NAME."
	;;
  restart|force-reload)
	echo -n "Restarting $DESC: "
	start-stop-daemon --stop --quiet --pidfile \
		/var/run/$NAME.pid --exec $DAEMON || true
	sleep 1

	start-stop-daemon --start --quiet --pidfile \
		/var/run/$NAME.pid --exec $DAEMON -- $DAEMON_OPTS || true
	echo "$NAME."
	;;
  reload)
        echo -n "Reloading $DESC configuration: "

        start-stop-daemon --stop --signal HUP --quiet --pidfile /var/run/$NAME.pid \
            --exec $DAEMON || true
        echo "$NAME."
        ;;
  configtest)
        echo -n "Testing $DESC configuration: "
        if test_nodekeeper_config
        then
          echo "$NAME."
        else
          exit $?
        fi
        ;;
  status)
	status_of_proc -p /var/run/$NAME.pid "$DAEMON" nodekeeper && exit 0 || exit $?
	;;
  *)
	echo "Usage: $NAME {start|stop|restart|reload|force-reload|status|configtest}" >&2
	exit 1
	;;
esac

exit 0
		
		

我們將使用DAEMON_OPTS變數,提供所有需要的參數

DAEMON_OPTS="--daemon --host $HOST --port $PORT --config=$CONFIG --protocol=$PROTOCOL --pidfile=$PIDFILE --logfile=$LOGFILE"
		

4.1. start/stop

/etc/init.d/nodekeeper start
/etc/init.d/nodekeeper stop
			

4.2. service start/stop

service nodekeeper start
service nodekeeper stop