Запуск локальных ssh/telnet/vnc клиентов по ссылке из карты Zabbix

    Множество стоек, каждая плотно упакована серверами, маршрутизаторами, коммутаторами и прочими kvm'ами.
    Нужен какой-нибудь удобный способ рулить всем этим хозайством, быстро подключаться к нужному оборудованию и
    производить его настройку. Прямо чтобы пара кликов мышью и оп — перед тобой консоль нужного коммутатора.

    Для мониторинга наших подопечных мы используем Zabbix.
    Так почему бы не приспособить сей дивный инструмент и для этой задачи.
    Ведь было бы очень удобно ткнуть в карте Zabbix на нужную стойку, перейти на её подкарту и, выбрав железку,
    запустить локальный ssh/telnet/vnc клиент на своем компьютере.

    Озадачившись идеей, я начал мучать поисковые машины в надежде отыскать варианты реализации.
    Был найден данный тред на форуме Zabbix, но мне хотелось запускать именно локальные программы на моей машине кликом по ссылке в карте.
    Еще некоторое время поплутав по закоулкам всемирной паутины и помучав знакомых программистов глупыми вопросами я вспомнил о… Python.
    Да, Python, не раз пришедший на помощь в трудную минуту.
    Питаю очень нежные чувства к этому языку за его простоту и приятный теплый синтаксис.

    И так, вектор атаки изменился и поисковики замерли в ожидании нового вброса мыслеобразов…
    Спустя некоторое время я уже четко представлял как буду решать задачу — напишу клиент-серверное приложение!
    На моем компьютере будет ждать команд серверная часть, а на сервере мониторинга, при клике по ссылке, будет запускаться клиент и передавать нужную команду.

    Результатом изысканий стало кроссплатформенное приложение, работает как на Linux, так и на Windows.
    Эпопея проб и ошибок на пути к заветной цели ждет вас под хабракатом.


    Пролог


    Начало пути.

    В первую очередь отмечу, что синтаксис написанного справедлив для Python версии 2.7.
    А версия Zabbix насчитала три двойки — Zabbix 2.2.2
    Отвечать за коммуникацию клиента и сервера поручено Python библиотеке socket.
    Помимо документации мне были полезны следующие источники:
    1. www.binarytides.com/python-socket-programming-tutorial
    2. www.binarytides.com/code-chat-application-server-client-sockets-python
    3. java-developer.livejournal.com/6920.html

    Наигравшись с примерами, подготовим основу будущего приложения.
    Клиент:
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import socket
    
    sock = socket.socket()
    sock.connect(('localhost', 9090))
    sock.send('/usr/bin/konsole -e  mc')
    
    sock.close()
    

    И сервер:
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import socket
    import subprocess
    
    sock = socket.socket()
    sock.bind(('localhost', 9090))
    sock.listen(1)
    while True:
            conn, addr = sock.accept()
            data = conn.recv(1024)
            if not data:
                    break
            subprocess.call(data, shell=True)
    conn.close()
    

    Запущеный сервер «слушает» порт 9090 на интерфейсе обратной петли, выполнив клиента мы передаем команду для запуска новой консоли, а параметр -e mc указывает, что в консоли нужно запустить файловый менеджер midnight commander.
    Краткая демонстрация


    *Основной ОС для меня является Linux, а в качестве DE выступает KDE, но пусть вас это не смущает, итоговый вариант скриптов не будет привязан к ОС.

    Основа положена, двигаемся далее!

    Глава 1


    В этой главе клиент научится понимать аргументы командной строки и мы отправим серверу первую команду с карты Zabbix.

    Код серверной части пока не претерпел никаких изменений, а вот в код клиента добавился импорт библиотеки sys.
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import socket
    import sys
    
    sock = socket.socket()
    sock.connect(('localhost', 9090))
    sock.send('/usr/bin/konsole -e ssh root@'+ sys.argv[1])
    
    sock.close()
    

    Сервер должен будет запустить консоль и инициировать подключение по ssh к IP из первого аргумента переданного клиенту.
    Логином выступает root.

    А теперь пришло время поместить скрипт клиента на сервер мониторинга.
    В моем случае он уютно устроился здесь — /usr/local/bin/client.py
    В веб интерфейсе Zabbix перейдем в Administration -> Scripts.

    Создадим новый скрипт(Create new).

    Через Zabbix макрос {HOST.IP} клиенту передается IP для подключения.
    Перейдя на карту увидим, что у хостов появилось меню с именем вновь созданного скрипта.

    Многие справедливо заметят — «Скрипт клиента до сих пор соединяется с localhost, но он же уже на сервере?!»
    Все верно! Но для защиты взаимодействия сервера и клиента используется ssh туннель.
    Все соединения устанавливаемые на localhost:9090 Zabbix сервера на самом деле устанавливаются со скриптом сервера на нашей локальной машине.
    Для достижения такого «зеркалирования» на локальном компьютере нужно выполнить команду:
    ssh -f -N -R 9090:127.0.0.1:9090  zabbix
    

    Подробнее о ssh, и тоннелях в часности, можно прочесть в замечательных статьях на хабре — 1, 2
    Видеоиллюстрация к первой главе



    Глава 2


    В этой главе скрипты еще немного поумнеют и научатся отличать Windows от Linux.

    Идея состоит в том, чтобы клиент при соединении с сервером первым делом узнавал на какой ОС тот запущен.
    И, получив это информацию, отдавал правильную команду.
    Код обоих скриптов изменился.

    Клиент:
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import socket
    import sys
    
    sock = socket.socket()
    sock.connect(('localhost', 9090))
    
    os = sock.recv(64)
    if  os == "Windows":
        sock.send('C:\\apps\\putty.exe -ssh root@' + sys.argv[1])
    elif os == "Linux":
        sock.send('/usr/bin/konsole -e ssh root@'+ sys.argv[1])
    
    sock.close()
    

    Сервер:
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import socket
    import subprocess
    
    OS = "Linux"
    
    sock = socket.socket()
    sock.bind(('127.0.0.1', 9090))
    sock.listen(1)
    while True:
        conn, addr = sock.accept()
        conn.sendall(OS)
        data = conn.recv(1024)
        print data
        if not data:
            break
        subprocess.call(data, shell=True)
    conn.close()
    

    * для Windows переменная OS должна быть изменена соответствующе
    Как видно, при коннекте к серверу клиент перво-наперво получает строку с названием ОС, и только потом посылает команду.
    Обратите внимание, что бекслэши в путях для ОС Windows нужно экранировать, т.е. ставить двойной \\

    Видео ко второй главе

    На видео:
    «зеркалируем» порт в Windows запустив putty
    запускаем сервер
    кликаем ссылку для соединения с хостом
    отключаем все в Windows и проделываем то же самое в Linux


    Глава 3


    В этой главе мы немного подправим код фронтенда Zabbix.
    Научим его передавать скрипту-клиенту вторым аргументом логин зарегистрированного на веб интерфейсе администратора/оператора.

    Вроде бы все здорово и уже сейчас можно пользоваться наработками для соединения с серверами, маршрутизаторами, коммутаторами.
    Но что если администраторов, пользующихся Zabbix, больше одного?
    Нам нужно каким-то образом дать понять скрипту кто его запустил, попросту говоря кто «ткнул» ссылку в веб интерфейсе.

    Решать будем поэтапно.
    • Закрепим за каждым администратором определенный порт.
    • Научим скрипт-клиент выбирать порт в зависимости от второго аргумента — логина.
    • Изменим код Zabbix для передачи второго аргумента при клике по ссылке из веб интерфейса.

    Порты:
    Admin — 9090
    Admin1 — 9091
    и т.д.

    Код скрипта-клиента с нужными изменениями:
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    import socket
    import sys
    
    if sys.argv[2] == 'Admin':
            PORT=9090
    elif sys.argv[2] == 'Admin1':
            PORT=9091
    
    sock = socket.socket()
    sock.connect(('localhost', PORT))
    os = sock.recv(64)
    
    if  os == "Windows":
            sock.send('C:\\apps\\putty.exe -ssh root@' + sys.argv[1])
    elif os == "Linux":
            sock.send('/usr/bin/konsole -e ssh root@'+ sys.argv[1])
    
    sock.close()
    


    А вот решить последнюю задачу, в рамках данной главы, оказалось не так просто.
    Пришлось существенно увеличить лимит кофе на эту смену и даже умыкнуть кота у жены из под бока.
    Кофе был выпит, кот, промурлыкав положенное, давно свалил, а я ни на йоту не приблизился к решению…
    Но ощущение близости отгадки не давало мне покоя, оно крепко вцепилось в мой мозг своими клешнями, и я знал, что яблоко вот-вот должно свалиться мне на голову.
    И оно свалилось!(не в прямом смысле конечно)
    Работающий вариант, как это часто бывает, был практически на поверхности.
    Для выполнения всех скриптов, вызванных из веб интерфейса, используется scripts_exec.php, расположеный в корневом каталоге установленного Zabbix.
    Описание скриптов хранится в MySQL базе.

    Значит исправив должным образом scripts_exec.php мы сможем передавать логин как второй аргумент для нашего скрипта-клиента при клике по ссылке.
    Код исправленного scripts_exec.php:
    <?php
    /*
    ** Zabbix
    ** Copyright (C) 2001-2014 Zabbix SIA
    **
    ** This program is free software; you can redistribute it and/or modify
    ** it under the terms of the GNU General Public License as published by
    ** the Free Software Foundation; either version 2 of the License, or
    ** (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    ** GNU General Public License for more details.
    **
    ** You should have received a copy of the GNU General Public License
    ** along with this program; if not, write to the Free Software
    ** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
    **/
    
    
    require_once dirname(__FILE__).'/include/config.inc.php';
    
    $page['title'] = _('Scripts');
    $page['file'] = 'scripts_exec.php';
    
    define('ZBX_PAGE_NO_MENU', 1);
    
    require_once dirname(__FILE__).'/include/page_header.php';
    
    $NewCommand4Run = '/usr/local/bin/client_ssh.py {HOST.IP} '. CWebUser::$data['alias'];
    
    
    // VAR  TYPE    OPTIONAL        FLAGS   VALIDATION      EXCEPTION
    $fields = array(
            'hostid' =>             array(T_ZBX_INT, O_OPT, P_ACT, DB_ID, null),
            'scriptid' =>   array(T_ZBX_INT, O_OPT, null, DB_ID, null)
    );
    check_fields($fields);
    
    ob_flush();
    flush();
    
    $scriptId = getRequest('scriptid');
    $hostId = getRequest('hostid');
    
    $data = array(
            'message' => '',
            'info' => DBfetch(DBselect('SELECT s.name FROM scripts s WHERE s.scriptid='.zbx_dbstr($scriptId)))
    );
    
    if ($scriptId == 5) {
                    DBexecute('update zabbix.scripts set command='."'".$NewCommand4Run."'".' where scriptid=5;');
            }
    
    $result = API::Script()->execute(array(
            'hostid' => $hostId,
            'scriptid' => $scriptId
    ));
    
    $isErrorExist = false;
    
    if (!$result) {
            $isErrorExist = true;
    }
    elseif ($result['response'] == 'failed') {
            error($result['value']);
    
            $isErrorExist = true;
    }
    else {
            $data['message'] = $result['value'];
    }
    
    if ($isErrorExist) {
            show_error_message(
                    _('Cannot connect to the trapper port of zabbix server daemon, but it should be available to run the script.')
            );
    }
    
    // render view
    $scriptView = new CView('general.script.execute', $data);
    $scriptView->render();
    $scriptView->show();
    
    require_once dirname(__FILE__).'/include/page_footer.php';
    


    Переменная
    $NewCommand4Run = '/usr/local/bin/client.py {HOST.IP} '. CWebUser::$data['alias'];
    

    после нажатия ссылки, будет содержать путь к скрипту + макрос {HOST.IP} + логин админа.
    А условие
    if ($scriptId == 5) {
                    DBexecute('update zabbix.scripts set command='."'".$NewCommand4Run."'".' where scriptid=5;');
            }
    

    заменит в таблице MySQL команду на нужную нам.
    Например вот так:


    И теперь скрипт-клиент сможет передать команду нужному скрипту-серверу.

    А вот как это выглядит.
    Смотреть лучше без звука, ноут у меня разбушевался уж очень)


    Глава 4


    В этой главе мы научим Zabbix запускать vnc и telnet клиентов.

    Поговорим о скриптах-клиентах для прочих, перечисленных в заголовке, протоколов.
    Листинг директории /usr/local/bin на Zabbix сервере:
    -rwxr-xr-x 1 root staff 429 Mar 17 03:06 client_ssh.py
    -rwxr-xr-x 1 root staff 418 Mar 17 04:58 client_telnet.py
    -rwxr-xr-x 1 root staff 412 Mar 17 05:00 client_vnc.py
    

    Листинг каждого скрипта-клиента
    client_ssh.py
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    import socket
    import sys
    
    if sys.argv[2] == 'Admin':
            PORT=9090
    elif sys.argv[2] == 'Admin1':
            PORT=9091
    
    sock = socket.socket()
    sock.connect(('localhost', PORT))
    os = sock.recv(64)
    
    if  os == "Windows":
            sock.send('C:\\apps\\putty.exe -ssh root@' + sys.argv[1])
    elif os == "Linux":
            sock.send('/usr/bin/konsole -e ssh root@'+ sys.argv[1])
    
    sock.close()
    

    client_vnc.py
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    import socket
    import sys
    
    if sys.argv[2] == 'Admin':
            PORT=9090
    elif sys.argv[2] == 'Admin1':
            PORT=9091
    
    sock = socket.socket()
    sock.connect(('localhost', PORT))
    os = sock.recv(64)
    
    if  os == "Windows":
            sock.send('c:\\apps\\vnc.exe ' + sys.argv[1])
    elif os == "Linux":
            sock.send('/usr/bin/X11/gvncviewer '+ sys.argv[1])
    
    sock.close()
    

    client_telnet.py
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import socket
    import sys
    
    if sys.argv[2] == 'Admin':
            PORT=9090
    elif sys.argv[2] == 'Admin1':
            PORT=9091
    
    sock = socket.socket()
    sock.connect(('localhost', PORT))
    
    os = sock.recv(64)
    if  os == "Windows":
        sock.send('C:\\apps\\putty.exe -telnet ' + sys.argv[1])
    elif os == "Linux":
        sock.send('/usr/bin/konsole -e telnet '+ sys.argv[1])
    
    
    sock.close()
    


    Подробно останавливаться на коде не будем, в предыдущих главах это уже было сделано.
    Скажу лишь, что для Windows я скачал portable версию vnc клиента и положил все в c:\apps\

    Для telnet и vnc из скриптов нужно повторить шаги из главы 1, в веб интерфейсе Zabbix, а для ssh достаточно скорректировать название.
    Файл scripts_exeс.php так же нуждается в изменении.
    Вновь измененный код scripts_exeс.php
    <?php
    /*
    ** Zabbix
    ** Copyright (C) 2001-2014 Zabbix SIA
    **
    ** This program is free software; you can redistribute it and/or modify
    ** it under the terms of the GNU General Public License as published by
    ** the Free Software Foundation; either version 2 of the License, or
    ** (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    ** GNU General Public License for more details.
    **
    ** You should have received a copy of the GNU General Public License
    ** along with this program; if not, write to the Free Software
    ** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
    **/
    
    
    require_once dirname(__FILE__).'/include/config.inc.php';
    
    $page['title'] = _('Scripts');
    $page['file'] = 'scripts_exec.php';
    
    define('ZBX_PAGE_NO_MENU', 1);
    
    require_once dirname(__FILE__).'/include/page_header.php';
    
    $NewCommand4RunSSH = '/usr/local/bin/client_ssh.py {HOST.IP} '. CWebUser::$data['alias'];
    $NewCommand4RunVNC = '/usr/local/bin/client_vnc.py {HOST.IP} '. CWebUser::$data['alias'];
    $NewCommand4RunTELNET = '/usr/local/bin/client_telnet.py {HOST.IP} '. CWebUser::$data['alias'];
    
    
    // VAR  TYPE    OPTIONAL        FLAGS   VALIDATION      EXCEPTION
    $fields = array(
            'hostid' =>             array(T_ZBX_INT, O_OPT, P_ACT, DB_ID, null),
            'scriptid' =>   array(T_ZBX_INT, O_OPT, null, DB_ID, null)
    );
    check_fields($fields);
    
    ob_flush();
    flush();
    
    $scriptId = getRequest('scriptid');
    $hostId = getRequest('hostid');
    
    $data = array(
            'message' => '',
            'info' => DBfetch(DBselect('SELECT s.name FROM scripts s WHERE s.scriptid='.zbx_dbstr($scriptId)))
    );
    
    if ($scriptId == 5) {
                    DBexecute('update zabbix.scripts set command='."'".$NewCommand4RunSSH."'".' where scriptid=5;');
            }
    if ($scriptId == 6) {
                    DBexecute('update zabbix.scripts set command='."'".$NewCommand4RunVNC."'".' where scriptid=6;');
            }
    if ($scriptId == 7) {
                    DBexecute('update zabbix.scripts set command='."'".$NewCommand4RunTELNET."'".' where scriptid=7;');
            }
    
    
    $result = API::Script()->execute(array(
            'hostid' => $hostId,
            'scriptid' => $scriptId
    ));
    
    $isErrorExist = false;
    
    if (!$result) {
            $isErrorExist = true;
    }
    elseif ($result['response'] == 'failed') {
            error($result['value']);
    
            $isErrorExist = true;
    }
    else {
            $data['message'] = $result['value'];
    }
    
    if ($isErrorExist) {
            show_error_message(
                    _('Cannot connect to the trapper port of zabbix server daemon, but it should be available to run the script.')
            );
    }
    
    // render view
    $scriptView = new CView('general.script.execute', $data);
    $scriptView->render();
    $scriptView->show();
    
    require_once dirname(__FILE__).'/include/page_footer.php';
    


    Уже традиционное видео.
    Здесь тоже лучше без звука.



    Заключение.


    Наведем порядок и придадим некоторый лоск нашей разработке.

    Строго говоря, здесь кросплатформеность и заканчивается.

    Для ОС Linux, из множества примеров, был собран демон на Python.
    Он выполняет дабл форк и преспокойно ждет команд от скриптов-клиентов.
    В ОС Windows все оказалось сложнее, но, обо всем по порядку.

    Листинг директории с необходимыми скриптами для ОС Linux:
    fessae@workbook:~/sh/python > pwd
    /home/fessae/sh/python
    fessae@workbook:~/sh/python > ll
    total 12
    -rwxr-xr-x 1 fessae fessae 2880 Feb 28 02:56 daemon.py*
    -rwxr-xr-x 1 fessae fessae  710 Mar 17 03:36 server.py*
    -rwxr-xr-x 1 fessae fessae 1122 Mar 13 04:59 ZabbixTeams.py*
    

    Код каждого из скриптов серверной части Linux.
    daemon.py
    #!/usr/bin/env python
    
    import sys, os, time, atexit
    from signal import SIGTERM
    
    class Daemon:
            """
            A generic daemon class.
    
            Usage: subclass the Daemon class and override the run() method
            """
            def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
                    self.stdin = stdin
                    self.stdout = stdout
                    self.stderr = stderr
                    self.pidfile = pidfile
    
            def daemonize(self):
                    """
                    do the UNIX double-fork magic, see Stevens' "Advanced
                    Programming in the UNIX Environment" for details (ISBN 0201563177)
                    http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
                    """
                    try:
                            pid = os.fork()
                            if pid > 0:
                                    # exit first parent
                                    sys.exit(0)
                    except OSError, e:
                            sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
                            sys.exit(1)
    
                    # decouple from parent environment
                    os.chdir("/")
                    os.setsid()
                    os.umask(0)
    
                    # do second fork
                    try:
                            pid = os.fork()
                            if pid > 0:
                                    # exit from second parent
                                    sys.exit(0)
                    except OSError, e:
                            sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
                            sys.exit(1)
    
                    # redirect standard file descriptors
                    sys.stdout.flush()
                    sys.stderr.flush()
                    si = file(self.stdin, 'r')
                    so = file(self.stdout, 'a+')
                    se = file(self.stderr, 'a+', 0)
                    os.dup2(si.fileno(), sys.stdin.fileno())
                    os.dup2(so.fileno(), sys.stdout.fileno())
                    os.dup2(se.fileno(), sys.stderr.fileno())
    
                    # write pidfile
                    atexit.register(self.delpid)
                    pid = str(os.getpid())
                    file(self.pidfile,'w+').write("%s\n" % pid)
    
            def delpid(self):
                    os.remove(self.pidfile)
    
            def start(self):
                    """
                    Start the daemon
                    """
                    # Check for a pidfile to see if the daemon already runs
                    try:
                            pf = file(self.pidfile,'r')
                            pid = int(pf.read().strip())
                            pf.close()
                    except IOError:
                            pid = None
    
                    if pid:
                            message = "pidfile %s already exist. Daemon already running?\n"
                            sys.stderr.write(message % self.pidfile)
                            sys.exit(1)
    
                    # Start the daemon
                    self.daemonize()
                    self.run()
    
            def stop(self):
                    """
                    Stop the daemon
                    """
                    # Get the pid from the pidfile
                    try:
                            pf = file(self.pidfile,'r')
                            pid = int(pf.read().strip())
                            pf.close()
                    except IOError:
                            pid = None
    
                    if not pid:
                            message = "pidfile %s does not exist. Daemon not running?\n"
                            sys.stderr.write(message % self.pidfile)
                            return # not an error in a restart
    
                    # Try killing the daemon process
                    try:
                            while 1:
                                    os.kill(pid, SIGTERM)
                                    time.sleep(0.1)
                    except OSError, err:
                            err = str(err)
                            if err.find("No such process") > 0:
                                    if os.path.exists(self.pidfile):
                                            os.remove(self.pidfile)
                            else:
                                    print str(err)
                                    sys.exit(1)
    
            def restart(self):
                    """
                    Restart the daemon
                    """
                    self.stop()
                    self.start()
    
    def run(self):
                    """
                    You should override this method when you subclass Daemon. It will be called after the process has been
                    daemonized by start() or restart().
            """
    

    server.py
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    import socket
    import subprocess
    import sys
    
    OS = "Linux"
    
    def main():
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            print 'Socket created'
            try:
                sock.bind(('127.0.0.1', 9090))
            except socket.error , msg:
                print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1]
                sys.exit()
            print 'Socket bind complete'
            sock.listen(1)
            print 'Socket now listening'
    
            while True:
                    conn, addr = sock.accept()
                    conn.sendall(OS)
                    data = conn.recv(1024)
                    print 'Connected with ' + addr[0] + ':' + str(addr[1])
                    print data
                    if not data:
                            break
                    subprocess.call(data, shell=True)
            conn.close()
    
    if __name__ == "__main__":
        main()
    

    ZabbixTeams.py
    #/usr/bin/env python
    #-*- coding: utf-8 -*-                                                                                                                           
    
    import sys
    sys.path.append("/home/fessae/sh/python")
    from daemon import Daemon
    import server
    
    class ZabbixTeams(Daemon):
                def run(self):
                    while True:
                        server.main()
                                
    
    if __name__ == "__main__":
                daemon = ZabbixTeams('/tmp/zabbixteams.pid',stdout='/tmp/zabbixteams.log',stderr='/tmp/zabbixteamserr.log')
                if len(sys.argv) == 2:
                        if 'start' == sys.argv[1]:
                                daemon.start()
                        elif 'stop' == sys.argv[1]:
                                daemon.stop()
                        elif 'restart' == sys.argv[1]:
                                daemon.restart()
                        else:
                                print "Unknown command"
                                sys.exit(2)
                        sys.exit(0)
                else:
                        print "usage: %s start|stop|restart" % sys.argv[0]
                        sys.exit(2)
    


    Для запуска демона набираем
    python ZabbixTeams.py start
    

    Для остановки соответственно
    python ZabbixTeams.py stop
    

    Обратите внимание на строку
    sys.path.append("/home/fessae/sh/python")
    

    из ZabbixTeams.py, она подключает врзможность импортировать скрипты из этого каталога.
    Исправьте её в соответствии вашему окружению.

    Получившийся демон можно стартовать руками, прилипить иконку для его запуска, завернуть в австостарт DE или ОС.
    Это уже кому как нравится.

    ОС Windows.
    Для написания/исправления кода я пользовался Python IDE PyCharm, поэтому и скрипты разместились по пути:


    Правильнее, конечно, было бы написать сервис для этой ОС при помощи pywin32.
    Но в скриптах использованы блокирующие сокеты и не отказавшись от них полноценный сервис у меня не получался.
    Возможно позже я возьмусь за asyncore или вообще перепишу все на twisted, но пока мне не хотелось усложнять.
    Поэтому запуск демона на Windows выглядит следующим образом.
    • на рабочем столе расположен файл server.bat содержащий:
       c:\Python27\python.exe C:\Users\FessAectan\PycharmProjects\ZabbixTeams\daemon.py
      

    • daemon.py, в свою очередь, выглядит вот так:
      import sys
      import subprocess
      CREATE_NO_WINDOW = 0x8000000
      subprocess.Popen(["c:\Python27\python.exe", "-O","C:\Users\FessAectan\PycharmProjects\ZabbixTeams\server.py"], shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags = CREATE_NO_WINDOW)
      

    • и, конечно же, сам server.py
      #!/usr/bin/env python
      # -*- coding: utf-8 -*-
      
      import socket
      import subprocess
      
      OS = "Windows"
      
      def main():
          sock = socket.socket()
          print "Socket created"
          try:
              sock.bind(('127.0.0.1', 9091))
          except socket.error , msg:
              print "Bind failed. Error Code : " + str(msg[0]) + " Message " + msg[1]
              sys.exit()
          print "Socket bind complete"
          sock.listen(1)
          print "Socket now listening"
              
          while True:
              conn, addr = sock.accept()
              conn.sendall(OS)
              data = conn.recv(1024)
              print "Connected with " + addr[0] + ":" + str(addr[1])
              if not data:
                  break
              subprocess.Popen(data, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags = 0x00000008)
          conn.close()
      
      
      if __name__ == "__main__":
              main()
      
      

      Для Windows subprocess.call был заменен на subprocess.Popen.



    Итоговое видео.
    *со звуком все в порядке ;)


    Благодарю за внимание.
    by FessAectan

    ServerClub
    ServerClub
    Company
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 10

      +2
      Не умаляя труда автора, я для той-же задачи использую расширение в браузер
        0
        Узнал об этом расширении уже когда все было готово и сел за статью.
        Глянул мельком, для того чтобы с карты подключаться, через расширение, тоже потрудиться основательно нужно.
          +3
          А ещё под виндой можно зарегистрировать telnet:// и ssh:// протоколы и запускать putty из любого приложения, умеющего открывать URLы. Да хоть бы «пуск»->«выполнить»->ssh://192.168.1.1/.
            0
            Аналогично на маке, только синтаксис:
            open telnet://
            open vnc://
            
            0
            А для хромиума не знаете подобного?
              0
              Не пользуюсь хромом и его форками, не подскажу.
            +3
            Отличный бэкдор.
            Правильнее было бы сделать чтобы передавался только минимальный набор параметров к konsole.
            Допустим тип клиента и адрес/порт. Иначе вам рано или поздно прилетит что-нибудь интересное на этот сервер. :-)
              0
              Серверная часть слушает loopback, но в дальнейшем, все же, думаю сделать проверку на стороне сервера, где «пускать» только определенный список команд.
              +1
              loopback — не повод не думать о безопасности. Всегда есть вероятность получить что-нибудь и по loopback.
                +1
                спасибо за статью, надо будет попробовать у себя!

                Only users with full accounts can post comments. Log in, please.