blob: 1e05d77a340f10dfb73d31967bc948816c69db7a [file] [log] [blame]
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +02001# Copyright (C) 2012, 2013 Holger Hans Peter Freyther
Kata7185c62013-04-04 17:31:13 +02002# Copyright (C) 2013 Katerina Barone-Adesi
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16#
17# VTY helper code for OpenBSC
18#
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +020019import re
Kata7185c62013-04-04 17:31:13 +020020import socket
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010021import sys, subprocess
Neels Hofmeyr2bdab3d2017-02-27 00:58:19 +010022import os
Neels Hofmeyrabd4b7d2017-02-27 01:03:44 +010023import time
Kata7185c62013-04-04 17:31:13 +020024
Kata8ee6bb2013-04-05 17:06:30 +020025"""VTYInteract: interact with an osmocom vty
26
27Specify a VTY to connect to, and run commands on it.
28Connections will be reestablished as necessary.
29Methods: __init__, command, enabled_command, verify, w_verify"""
30
Neels Hofmeyr2bdab3d2017-02-27 00:58:19 +010031debug_tcp_sockets = (os.getenv('OSMOPY_DEBUG_TCP_SOCKETS', '0') != '0')
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010032
33def cmd(what):
34 print '\n> %s' % what
35 sys.stdout.flush()
36 subprocess.call(what, shell=True)
37 sys.stdout.flush()
38 sys.stderr.flush()
39 print ''
40 sys.stdout.flush()
41
42def print_used_tcp_sockets():
Neels Hofmeyrcb320b82017-02-27 01:11:13 +010043 global debug_tcp_sockets
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010044 if not debug_tcp_sockets:
45 return
Holger Hans Peter Freytherf41db1e2017-09-13 15:42:15 +080046 cmd('ls -l /proc/' + str(os.getpid()) + '/fd');
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010047 cmd('ss -tn');
48 cmd('ss -tln');
49 cmd('ps xua | grep osmo');
Kata7185c62013-04-04 17:31:13 +020050
51class VTYInteract(object):
Kata8ee6bb2013-04-05 17:06:30 +020052 """__init__(self, name, host, port):
53
54 name is the name the vty prints for commands, ie OpenBSC
55 host is the hostname to connect to
56 port is the port to connect on"""
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010057
58 all_sockets = []
59
Kata7185c62013-04-04 17:31:13 +020060 def __init__(self, name, host, port):
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010061 print_used_tcp_sockets()
62
Kata7185c62013-04-04 17:31:13 +020063 self.name = name
64 self.host = host
65 self.port = port
66
67 self.socket = None
Jacob Erlbeck41b0d302013-08-30 18:28:05 +020068 self.norm_end = re.compile('\r\n%s(?:\(([\w-]*)\))?> $' % self.name)
69 self.priv_end = re.compile('\r\n%s(?:\(([\w-]*)\))?# $' % self.name)
70 self.last_node = ''
Kata7185c62013-04-04 17:31:13 +020071
Neels Hofmeyr4e64b882017-02-27 01:31:02 +010072 def _connect_socket(self):
73 if self.socket is not None:
74 return
Neels Hofmeyrabd4b7d2017-02-27 01:03:44 +010075 retries = 30
76 took = 0
77 while True:
78 took += 1
79 try:
80 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
81 self.socket.setblocking(1)
82 self.socket.connect((self.host, self.port))
83 except IOError:
84 retries -= 1
85 if retries <= 0:
86 raise
87 # possibly the binary hasn't launched yet
88 if debug_tcp_sockets:
89 print "Connecting socket failed, retrying..."
90 time.sleep(.1)
91 continue
92 break
93
Neels Hofmeyr4e64b882017-02-27 01:31:02 +010094 if debug_tcp_sockets:
95 VTYInteract.all_sockets.append(self.socket)
Neels Hofmeyrabd4b7d2017-02-27 01:03:44 +010096 print "Socket: in %d tries, connected to %s:%d %r (%d sockets open)" % (
97 took, self.host, self.port, self.socket,
Neels Hofmeyr4e64b882017-02-27 01:31:02 +010098 len(VTYInteract.all_sockets))
99 self.socket.recv(4096)
100
Kata7185c62013-04-04 17:31:13 +0200101 def _close_socket(self):
Neels Hofmeyrcb320b82017-02-27 01:11:13 +0100102 global debug_tcp_sockets
Neels Hofmeyre3493202017-02-27 01:12:14 +0100103 if self.socket is None:
104 return
105
106 if debug_tcp_sockets:
Neels Hofmeyr8e9f30f2017-02-27 01:17:22 +0100107 try:
108 VTYInteract.all_sockets.remove(self.socket)
109 except ValueError:
110 pass
Neels Hofmeyre3493202017-02-27 01:12:14 +0100111 print "Socket: closing %s:%d %r (%d sockets open)" % (
112 self.host, self.port, self.socket,
113 len(VTYInteract.all_sockets))
114 self.socket.close()
115 self.socket = None
Kata7185c62013-04-04 17:31:13 +0200116
117 def _is_end(self, text, ends):
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200118 """
119 >>> vty = VTYInteract('OsmoNAT', 'localhost', 9999)
120 >>> end = [vty.norm_end, vty.priv_end]
121
122 Simple test
123 >>> text1 = 'abc\\r\\nOsmoNAT> '
124 >>> vty._is_end(text1, end)
125 11
126
127 Simple test with the enabled node
128 >>> text2 = 'abc\\r\\nOsmoNAT# '
129 >>> vty._is_end(text2, end)
130 11
131
132 Now the more complicated one
133 >>> text3 = 'abc\\r\\nOsmoNAT(config)# '
134 >>> vty._is_end(text3, end)
135 19
136
137 Now the more complicated one
138 >>> text4 = 'abc\\r\\nOsmoNAT(config-nat)# '
139 >>> vty._is_end(text4, end)
140 23
141
142 Now the more complicated one
143 >>> text5 = 'abc\\r\\nmoo'
144 >>> vty._is_end(text5, end)
145 0
Jacob Erlbeck41b0d302013-08-30 18:28:05 +0200146
147 Check for node name extraction
148 >>> text6 = 'abc\\r\\nOsmoNAT(config-nat)# '
149 >>> vty._is_end(text6, end)
150 23
151 >>> vty.node()
152 'config-nat'
153
154 Check for empty node name extraction
155 >>> text7 = 'abc\\r\\nOsmoNAT# '
156 >>> vty._is_end(text7, end)
157 11
158 >>> vty.node() is None
159 True
160
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200161 """
Jacob Erlbeck41b0d302013-08-30 18:28:05 +0200162 self.last_node = None
Kata7185c62013-04-04 17:31:13 +0200163 for end in ends:
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200164 match = end.search(text)
165 if match:
Jacob Erlbeck41b0d302013-08-30 18:28:05 +0200166 self.last_node = match.group(1)
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200167 return match.end() - match.start()
168 return 0
Kata7185c62013-04-04 17:31:13 +0200169
170 def _common_command(self, request, close=False, ends=None):
Neels Hofmeyrcb320b82017-02-27 01:11:13 +0100171 global debug_tcp_sockets
Kata7185c62013-04-04 17:31:13 +0200172 if not ends:
173 ends = [self.norm_end, self.priv_end]
Neels Hofmeyr4e64b882017-02-27 01:31:02 +0100174
175 self._connect_socket()
Kata7185c62013-04-04 17:31:13 +0200176
177 # Now send the command
178 self.socket.send("%s\r" % request)
179 res = ""
180 end = ""
181
182 # Unfortunately, timeout and recv don't always play nicely
183 while True:
184 data = self.socket.recv(4096)
185 res = "%s%s" % (res, data)
186 if not res: # yes, this is ugly
187 raise IOError("Failed to read data (did the app crash?)")
188 end = self._is_end(res, ends)
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200189 if end > 0:
Kata7185c62013-04-04 17:31:13 +0200190 break
191
192 if close:
193 self._close_socket()
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200194 return res[len(request) + 2: -end]
Kata7185c62013-04-04 17:31:13 +0200195
Alexander Chemeris2f483132015-05-30 10:07:53 -0400196 """A generator function yielding lines separated by delim.
197 Behaves similar to a file readlines() method.
198
199 Example of use:
200 for line in vty.readlines():
201 print line
202 """
203 def readlines(self, recv_buffer=4096, delim='\n'):
204 buffer = ''
205 data = True
206 while data:
207 data = self.socket.recv(recv_buffer)
208 buffer += data
209
210 while buffer.find(delim) != -1:
211 line, buffer = buffer.split('\n', 1)
212 yield line
213 return
214
Kata7185c62013-04-04 17:31:13 +0200215 # There's no close parameter, as close=True makes this useless
216 def enable(self):
217 self.command("enable")
218
219 """Run a command on the vty"""
Kata8ee6bb2013-04-05 17:06:30 +0200220
Kata7185c62013-04-04 17:31:13 +0200221 def command(self, request, close=False):
222 return self._common_command(request, close)
223
224 """Run enable, followed by another command"""
225 def enabled_command(self, request, close=False):
226 self.enable()
227 return self._common_command(request, close)
228
229 """Verify, ignoring leading/trailing whitespace"""
230 # inspired by diff -w, though not identical
231 def w_verify(self, command, results, close=False, loud=True):
232 return self.verify(command, results, close, loud, lambda x: x.strip())
233
234 """Verify that a command has the expected results
235
236 command = the command to verify
237 results = the expected results [line1, line2, ...]
238 close = True to close the socket after running the verify
239 loud = True to show what was expected and what actually happend, stdout
240 f = A function to run over the expected and actual results, before compare
241
242 Returns True iff the expected and actual results match"""
243 def verify(self, command, results, close=False, loud=True, f=None):
244 res = self.command(command, close).split('\r\n')
245 if f:
246 res = map(f, res)
247 results = map(f, results)
248
249 if loud:
250 if res != results:
251 print "Rec: %s\nExp: %s" % (res, results)
252
253 return res == results
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200254
Jacob Erlbeck41b0d302013-08-30 18:28:05 +0200255 def node(self):
256 return self.last_node
257
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200258if __name__ == "__main__":
259 import doctest
260 doctest.testmod()