Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 15.06.2014 | нет комментариев
Множество упрям, всякая плотно упакована серверами, маршрутизаторами, коммутаторами и прочими 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.

Код серверной части пострированного на веб интерфейсе администратора/оператора.

Как бы бы все здорово и теснее теперь дозволено пользоваться наработками для соединения с серверами, маршрутизаторами, коммутаторами.
Но что если менеджеров, пользующихся 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 мы сумеем передавать логин как 2-й довод для нашего скрипта-заказчика при клике по ссылке.

Код поправленного 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("%sn" % 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:Python27python.exe C:UsersFessAectanPycharmProjectsZabbixTeamsdaemon.py
    
  • daemon.py, в свою очередь, выглядит вот так:
    import sys
    import subprocess
    CREATE_NO_WINDOW = 0x8000000
    subprocess.Popen(["c:Python27python.exe", "-O","C:UsersFessAectanPycharmProjectsZabbixTeamsserver.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

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB