📜 ⬆️ ⬇️

Running local ssh / telnet / vnc clients using the link from the Zabbix card

Many racks, each tightly packed with servers, routers, switches and other kvm'ami.
We need some convenient way to steer all this, quickly connect to the necessary equipment and
set it up. Right to a couple of mouse clicks and op - in front of you the console of the desired switch.

To monitor our charges we use Zabbix.
So why not adapt this marvelous tool for this task.
After all, it would be very convenient to poke the necessary rack in the Zabbix map, go to its submap and, choosing a piece of metal,
run local ssh / telnet / vnc client on your computer.

Puzzled by the idea, I began to torment the search engines in the hope of finding options for implementation.
This thread was found on the Zabbix forum, but I wanted to run local programs on my machine by clicking the link in the map.
For some time, wandering around the back streets of the world wide web and torturing familiar programmers with stupid questions, I remembered about ... Python.
Yes, Python, not once came to the rescue in a difficult moment.
I feed very tender feelings for this language for its simplicity and pleasant warm syntax.
')
And so, the attack vector changed and the search engines froze in anticipation of a new injection of thought patterns ...
After some time, I had a clear idea how I would solve the problem - I will write a client-server application!
On my computer, the server side will wait for commands, and on the monitoring server, when the user clicks on the link, the client will start and transfer the necessary command.

The result of the research was a cross-platform application that works on both Linux and Windows.
The epic of trial and error on the way to the cherished goal is waiting for you under the habrakat.


Prologue


The beginning of the way.

First of all, I note that the syntax is valid for Python version 2.7.
And the Zabbix version counted three twos - Zabbix 2.2.2
Responsible for client and server communication is entrusted to the socket library Python .
In addition to the documentation, the following sources were helpful to me:
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

Having played enough with examples, we will prepare the basis of the future application.
Customer:
 #!/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() 

And the server:
 #!/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() 

The running server “listens” to port 9090 on the loopback interface, having executed the client, we send the command to start the new console, and the -e mc parameter indicates that the midnight commander file manager should be started in the console.
Brief demonstration


* The main OS for me is Linux, and DE is KDE, but don't let that bother you, the final version of the scripts will not be tied to the OS.

The basis is laid, moving on!

Chapter 1


In this chapter, the client will learn to understand the command line arguments and we will send the first command to the server from the Zabbix map.

The server part code has not yet undergone any changes, but the import of the sys library has been added to the client code.
 #!/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() 

The server will have to start the console and initiate an ssh connection to IP from the first argument passed to the client.
Login is root .

And now it's time to put the client script on the monitoring server.
In my case, he comfortably settled here - /usr/local/bin/client.py
In the Zabbix web interface, go to Administration -> Scripts.

Create a new script (Create new).

Through Zabbix, the macro {HOST.IP} passes the IP of the client to the connection.
Turning to the map, we will see that the hosts have a menu with the name of the newly created script.

Many will rightly notice - “The client script is still connected to localhost, but is it already on the server ?!”
That's right! But to protect the interaction between the server and the client, an ssh tunnel is used.
All connections established on localhost: 9090 Zabbix server are actually established with the server script on our local machine.
To achieve such a "mirroring" on the local computer, run the command:
 ssh -f -N -R 9090:127.0.0.1:9090 zabbix 

Read more about ssh, and tunnels in the above privacy, you can read in the wonderful articles on Habré - 1 , 2
Video illustration for the first chapter



Chapter 2


In this chapter, the scripts will widen a little more and learn to distinguish between Windows and Linux.

The idea is that the client, when connecting to the server, first of all find out on which OS it is running.
And, having received this information, I gave the right command.
The code for both scripts has changed.

Customer:
 #!/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() 

Server:
 #!/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() 

* for Windows, the OS variable must be changed accordingly.
As you can see, when connecting to the server, the client first of all receives a string with the name of the OS, and only then sends the command.
Please note that backslashes in paths for Windows OS need to be escaped, i.e. put double \\

Video to the second chapter

In the video:
"Mirror" the port in Windows by running putty
we start the server
click the link to connect to the host
disable everything in Windows and do the same in Linux


Chapter 3


In this chapter we will slightly correct the code of the Zabbix frontend.
We will teach him to transfer to the script client the second argument the login of the administrator / operator registered on the web interface.

It seems to be all great and now you can use the operating time to connect to servers, routers, switches.
But what if there are more than one administrators using Zabbix?
We need to somehow make it clear to the script who launched it, simply speaking, who “poked” the link in the web interface.

We will decide in stages.

Ports:
Admin - 9090
Admin1 - 9091
etc.

The script code of the client with the necessary changes:
 #!/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() 


But to solve the last problem, in the framework of this chapter, it was not so easy.
I had to significantly increase the coffee limit for this shift, and even snitch a cat from my wife from under my side.
The coffee was drunk, the cat, having become purred, had long since been dumped, but I didn’t come close to a decision a jot ...
But the feeling of closeness otgadki did not give me rest, it firmly grabbed my brain with its claws, and I knew that the apple was about to fall on my head.
And it fell! (Not in the literal sense of course)
The working option, as is often the case, was practically on the surface.
To execute all the scripts called from the web interface, use scripts_exec.php located in the root directory of the installed Zabbix.
The description of the scripts is stored in the MySQL database.

So by correcting scripts_exec.php properly, we will be able to pass the login as the second argument for our client script when clicking on the link.
Code of the corrected 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'; 


Variable
 $NewCommand4Run = '/usr/local/bin/client.py {HOST.IP} '. CWebUser::$data['alias']; 

after clicking the link, it will contain the path to the script + macro {HOST.IP} + admin login.
A condition
 if ($scriptId == 5) { DBexecute('update zabbix.scripts set command='."'".$NewCommand4Run."'".' where scriptid=5;'); } 

will replace the command in the MySQL table with the one we need.
For example, like this:


And now the script client will be able to transfer the command to the desired server script.

And this is how it looks.
Look better without sound, I have a lot of laptop.)


Chapter 4


In this chapter, we will teach Zabbix to run vnc and telnet clients.

Let's talk about client scripts for other protocols listed in the header.
Listing of the / usr / local / bin directory on Zabbix server:
 -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 

Listing of each client script
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() 


We will not dwell on the code in detail; this has already been done in previous chapters.
Let me just say that for Windows I downloaded the portable version of the vnc client and put everything in c:\apps\

For telnet and vnc from the scripts, you need to repeat the steps from Chapter 1, in the Zabbix web interface, and for ssh you just need to correct the name.
The scripts_exe.php file also needs to be modified.
The newly modified code scripts_exes.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'; 


Already traditional video.
Here, too, better without sound.



Conclusion


We will restore order and give some gloss to our design.

Strictly speaking, this is where the cross-platform ends.

For Linux, from a variety of examples, a demon was built in Python.
He performs a double fork and calmly waits for commands from client scripts.
In Windows, everything was more complicated, but, first things first.

Listing directory with the necessary scripts for Linux OS:
 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* 

Code for each of the Linux server-side scripts.
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) 


To start the daemon we type
 python ZabbixTeams.py start 

To stop accordingly
 python ZabbixTeams.py stop 

Pay attention to the line
 sys.path.append("/home/fessae/sh/python") 

from ZabbixTeams.py, it connects the ability to import scripts from this directory.
Correct it according to your environment.

The resulting demon can be started by hand, stick the icon to start it, wrap in Austostart DE or OS.
This is someone as you like.

OS Windows.
For writing / correcting the code, I used Python IDE PyCharm, so the scripts were located along the path:


It would be more correct, of course, to write a service for this OS using pywin32 .
But the scripts used blocking sockets and I didn’t get a full service without abandoning them.
Maybe later I will take on asyncore or even rewrite everything in twisted, but so far I did not want to complicate things.
Therefore, running the daemon on Windows looks like this.
  • on the desktop is the server.bat file containing:
      c:\Python27\python.exe C:\Users\FessAectan\PycharmProjects\ZabbixTeams\daemon.py 

  • daemon.py, in turn, looks like this:
     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) 

  • and, of course, server.py itself
     #!/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() 

    For Windows, subprocess.call has been replaced by subprocess.Popen.



The final video.
* sound is all right;)


Thank you for attention.
by FessAectan

Serverclub

Source: https://habr.com/ru/post/217161/


All Articles