Skip to content

Commit 819714a

Browse files
author
Thane Thomson
committed
first commit
0 parents  commit 819714a

File tree

8 files changed

+351
-0
lines changed

8 files changed

+351
-0
lines changed

AUTHORS

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Praekelt Foundation
2+
===================
3+
* Thane Thomson
4+

LICENSE

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Copyright (c) 2011 Praekelt Foundation
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without
5+
modification, are permitted provided that the following conditions are met:
6+
* Redistributions of source code must retain the above copyright
7+
notice, this list of conditions and the following disclaimer.
8+
* Redistributions in binary form must reproduce the above copyright
9+
notice, this list of conditions and the following disclaimer in the
10+
documentation and/or other materials provided with the distribution.
11+
* Neither the name of Praekelt Foundation nor the
12+
names of its contributors may be used to endorse or promote products
13+
derived from this software without specific prior written permission.
14+
15+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18+
DISCLAIMED. IN NO EVENT SHALL PRAEKELT FOUNDATION BE LIABLE FOR ANY
19+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25+

MANIFEST.in

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include AUTHORS
2+
include LICENSE
3+
include README.rst
4+

README.rst

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
python-sshtail
2+
==============
3+
4+
A simple set of Python classes to facilitate tailing of one or more files via SSH.
5+
At the moment it only supports key-based SSH'ing.
6+
7+
Quick installation
8+
------------------
9+
Install from PyPI:
10+
11+
::
12+
13+
> easy_install -U python-sshtail
14+
15+
16+
Tailing a single file
17+
---------------------
18+
19+
::
20+
21+
from sshtail import SSHTailer
22+
from time import sleep
23+
24+
# "1.2.3.4" is the IP address or host name you want to access
25+
tailer = SSHTailer('1.2.3.4', '/var/log/path/to/my/logfile.log')
26+
27+
try:
28+
while 1:
29+
for line in tailer.tail():
30+
print line
31+
32+
# wait a bit
33+
time.sleep(1)
34+
35+
except:
36+
tailer.disconnect()
37+
38+
39+
Tailing multiple files
40+
----------------------
41+
42+
::
43+
44+
from sshtail import SSHMultiTailer
45+
46+
tailer = SSHMultiTailer({
47+
'1.2.3.4': ['/path/to/log1.log', '/path/to/log2.log'],
48+
'4.3.2.1': ['/path/to/log3.log'],
49+
})
50+
51+
# will run until it receives SIGINT, after which it will
52+
# automatically catch the exception, disconnect from the
53+
# remote hosts and perform cleanup
54+
55+
for host, filename, line in tailer.tail():
56+
print "%s:%s - %s" % (host, filename, line)
57+
58+
59+
60+
Using a custom private key
61+
--------------------------
62+
63+
::
64+
65+
from sshtail import SSHMultiTailer, load_dss_key
66+
67+
# if no path's specified for the private key file name,
68+
# it automatically prepends /home/<current_user>/.ssh/
69+
# and for RSA keys, import load_rsa_key instead.
70+
71+
tailer = SSHMultiTailer({
72+
'1.2.3.4': ['/path/to/log1.log', '/path/to/log2.log'],
73+
'4.3.2.1': ['/path/to/log3.log'],
74+
},
75+
private_key=load_dss_key('identity'))
76+
77+
for host, filename, line in tailer.tail():
78+
print "%s:%s - %s" % (host, filename, line)
79+
80+
81+
82+

setup.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from setuptools import setup, find_packages
2+
3+
setup(
4+
name='python-sshtail',
5+
version='0.0.1',
6+
description='Python classes to allow for tailing of multiple files via SSH.',
7+
long_description=open('README.rst', 'rt').read(),
8+
author='Praekelt Foundation',
9+
author_email='[email protected]',
10+
license='BSD',
11+
url='https://github.com/praekelt/python-sshtail',
12+
packages=find_packages(),
13+
install_requires=[
14+
'paramiko',
15+
],
16+
include_package_data=True,
17+
classifiers = [
18+
'Programming Language :: Python',
19+
'License :: OSI Approved :: BSD License',
20+
'Development Status :: 4 - Beta',
21+
'Operating System :: OS Independent',
22+
'Framework :: Django',
23+
'Intended Audience :: Developers',
24+
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
25+
],
26+
zip_safe=False,
27+
)
28+

sshtail/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from tailers import *
2+
from utils import *
3+

sshtail/tailers.py

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#
2+
# Library to handle tailing of files via SSH.
3+
#
4+
5+
#
6+
7+
import os
8+
import time
9+
import paramiko
10+
11+
12+
class SSHTailer(object):
13+
"""
14+
Class to handle the tailing of a single file via SSH.
15+
"""
16+
17+
def __init__(self, host, remote_filename, private_key=None):
18+
self.host = host
19+
self.remote_filename = remote_filename
20+
self.private_key = private_key
21+
self.client = None
22+
self.sftp_client = None
23+
self.remote_file_size = None
24+
self.line_terminators = ['\r', '\n', '\r\n']
25+
self.line_terminators_joined = '\r\n'
26+
27+
28+
def connect(self):
29+
print "Connecting to %s..." % self.host
30+
# connect to the host
31+
self.client = paramiko.SSHClient()
32+
self.client.load_system_host_keys()
33+
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
34+
if self.private_key:
35+
self.client.connect(self.host, pkey=self.private_key)
36+
else:
37+
self.client.connect(self.host)
38+
39+
print "Opening remote file %s..." % self.remote_filename
40+
# open a connection to the remote file via SFTP
41+
self.sftp_client = self.client.open_sftp()
42+
43+
44+
45+
def tail(self):
46+
# make sure there's a connection
47+
if not self.sftp_client:
48+
self.connect()
49+
50+
fstat = self.sftp_client.stat(self.remote_filename)
51+
52+
# check if we have the file size
53+
if self.remote_file_size is not None:
54+
# if the file's grown
55+
if self.remote_file_size < fstat.st_size:
56+
for line in self.get_new_lines():
57+
yield line
58+
59+
self.remote_file_size = fstat.st_size
60+
61+
62+
63+
def get_new_lines(self):
64+
"""
65+
Opens the file and reads any new data from it.
66+
"""
67+
68+
remote_file = self.sftp_client.open(self.remote_filename, 'r')
69+
# seek to the latest read point in the file
70+
remote_file.seek(self.remote_file_size, 0)
71+
# read any new lines from the file
72+
line = remote_file.readline()
73+
while line:
74+
yield line.strip(self.line_terminators_joined)
75+
line = remote_file.readline()
76+
77+
remote_file.close()
78+
79+
80+
81+
def disconnect(self):
82+
if self.sftp_client:
83+
print "Closing SFTP connection..."
84+
self.sftp_client.close()
85+
self.sftp_client = None
86+
if self.client:
87+
print "Closing SSH connection..."
88+
self.client.close()
89+
self.client = None
90+
91+
92+
93+
94+
class SSHMultiTailer(object):
95+
"""
96+
Class to handle tailing of multiple files.
97+
"""
98+
99+
def __init__(self, host_files, poll_interval=2.0, private_key=None):
100+
"""
101+
host_files is a dictionary whose keys must correspond to unique
102+
remote hosts to which this machine has access (ideally via SSH key).
103+
The values of the host_files dictionary must be arrays of file names
104+
that must be tailed.
105+
"""
106+
107+
self.host_files = host_files
108+
self.poll_interval = poll_interval
109+
self.private_key = private_key
110+
self.tailers = {}
111+
112+
113+
def connect(self):
114+
"""
115+
Connects to all of the host machines.
116+
"""
117+
118+
print "Connecting to multiple hosts..."
119+
120+
for host, files in self.host_files.iteritems():
121+
self.tailers[host] = {}
122+
for f in files:
123+
self.tailers[host][f] = SSHTailer(host, f, private_key=self.private_key)
124+
125+
126+
127+
def tail(self, report_sleep=False):
128+
"""
129+
Tails all of the requested files across all of the hosts.
130+
"""
131+
132+
# make sure we're connected
133+
if not self.tailers:
134+
self.connect()
135+
136+
try:
137+
# assuming this script is to run until someone kills it (Ctrl+C)
138+
while 1:
139+
lines_read = 0
140+
141+
for host, tailers in self.tailers.iteritems():
142+
for filename, tailer in tailers.iteritems():
143+
# read as much data as we can from the file
144+
for line in tailer.tail():
145+
yield host, filename, line
146+
lines_read += 1
147+
148+
if not lines_read:
149+
if report_sleep:
150+
yield None, None, None
151+
self.sleep()
152+
153+
finally:
154+
self.disconnect()
155+
156+
157+
158+
def sleep(self):
159+
time.sleep(self.poll_interval)
160+
161+
162+
163+
def disconnect(self):
164+
"""
165+
Disconnects all active connections.
166+
"""
167+
168+
for host, tailers in self.tailers.iteritems():
169+
for filename, tailer in tailers.iteritems():
170+
tailer.disconnect()
171+
172+
self.tailers = {}
173+
174+
print "Disconnected from hosts."
175+
176+
177+

sshtail/utils.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import os
2+
import paramiko
3+
4+
5+
def prepend_home_dir(filename):
6+
"""
7+
Prepends the home directory to the given filename if it doesn't
8+
already contain some kind of directory path.
9+
"""
10+
return os.path.join(os.environ['HOME'], '.ssh', filename) if '/' not in filename else filename
11+
12+
13+
14+
def load_rsa_key(filename):
15+
"""
16+
Function to get an RSA key from the specified file for Paramiko.
17+
"""
18+
19+
return paramiko.RSAKey.from_private_key_file(prepend_home_dir(filename))
20+
21+
22+
23+
def load_dss_key(filename):
24+
"""
25+
Function to get a DSS key from the specified file for Paramiko.
26+
"""
27+
28+
return paramiko.DSSKey.from_private_key_file(prepend_home_dir(filename))

0 commit comments

Comments
 (0)