blob: d39d3da8ccd2bee34a972f632d68f2bd6633c7ce [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#
Max6ccd0782017-12-15 12:15:39 +010019from __future__ import print_function
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +020020import re
Kata7185c62013-04-04 17:31:13 +020021import socket
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010022import sys, subprocess
Neels Hofmeyr2bdab3d2017-02-27 00:58:19 +010023import os
Neels Hofmeyrabd4b7d2017-02-27 01:03:44 +010024import time
Kata7185c62013-04-04 17:31:13 +020025
Kata8ee6bb2013-04-05 17:06:30 +020026"""VTYInteract: interact with an osmocom vty
27
28Specify a VTY to connect to, and run commands on it.
29Connections will be reestablished as necessary.
30Methods: __init__, command, enabled_command, verify, w_verify"""
31
Neels Hofmeyr2bdab3d2017-02-27 00:58:19 +010032debug_tcp_sockets = (os.getenv('OSMOPY_DEBUG_TCP_SOCKETS', '0') != '0')
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010033
34def cmd(what):
Max6ccd0782017-12-15 12:15:39 +010035 print('\n> %s' % what)
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010036 sys.stdout.flush()
37 subprocess.call(what, shell=True)
38 sys.stdout.flush()
39 sys.stderr.flush()
Max6ccd0782017-12-15 12:15:39 +010040 print('')
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010041 sys.stdout.flush()
42
43def print_used_tcp_sockets():
Neels Hofmeyrcb320b82017-02-27 01:11:13 +010044 global debug_tcp_sockets
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010045 if not debug_tcp_sockets:
46 return
Holger Hans Peter Freytherf41db1e2017-09-13 15:42:15 +080047 cmd('ls -l /proc/' + str(os.getpid()) + '/fd');
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010048 cmd('ss -tn');
49 cmd('ss -tln');
50 cmd('ps xua | grep osmo');
Kata7185c62013-04-04 17:31:13 +020051
52class VTYInteract(object):
Kata8ee6bb2013-04-05 17:06:30 +020053 """__init__(self, name, host, port):
54
55 name is the name the vty prints for commands, ie OpenBSC
56 host is the hostname to connect to
57 port is the port to connect on"""
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010058
59 all_sockets = []
60
Kata7185c62013-04-04 17:31:13 +020061 def __init__(self, name, host, port):
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010062 print_used_tcp_sockets()
63
Kata7185c62013-04-04 17:31:13 +020064 self.name = name
65 self.host = host
66 self.port = port
67
68 self.socket = None
Jacob Erlbeck41b0d302013-08-30 18:28:05 +020069 self.norm_end = re.compile('\r\n%s(?:\(([\w-]*)\))?> $' % self.name)
70 self.priv_end = re.compile('\r\n%s(?:\(([\w-]*)\))?# $' % self.name)
71 self.last_node = ''
Kata7185c62013-04-04 17:31:13 +020072
Neels Hofmeyr4e64b882017-02-27 01:31:02 +010073 def _connect_socket(self):
74 if self.socket is not None:
75 return
Neels Hofmeyrabd4b7d2017-02-27 01:03:44 +010076 retries = 30
77 took = 0
78 while True:
79 took += 1
80 try:
81 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
82 self.socket.setblocking(1)
83 self.socket.connect((self.host, self.port))
84 except IOError:
85 retries -= 1
86 if retries <= 0:
87 raise
88 # possibly the binary hasn't launched yet
89 if debug_tcp_sockets:
Max6ccd0782017-12-15 12:15:39 +010090 print("Connecting socket failed, retrying...")
Neels Hofmeyrabd4b7d2017-02-27 01:03:44 +010091 time.sleep(.1)
92 continue
93 break
94
Neels Hofmeyr4e64b882017-02-27 01:31:02 +010095 if debug_tcp_sockets:
96 VTYInteract.all_sockets.append(self.socket)
Max6ccd0782017-12-15 12:15:39 +010097 print("Socket: in %d tries, connected to %s:%d %r (%d sockets open)" % (
Neels Hofmeyrabd4b7d2017-02-27 01:03:44 +010098 took, self.host, self.port, self.socket,
Max6ccd0782017-12-15 12:15:39 +010099 len(VTYInteract.all_sockets)))
Neels Hofmeyr4e64b882017-02-27 01:31:02 +0100100 self.socket.recv(4096)
101
Kata7185c62013-04-04 17:31:13 +0200102 def _close_socket(self):
Neels Hofmeyrcb320b82017-02-27 01:11:13 +0100103 global debug_tcp_sockets
Neels Hofmeyre3493202017-02-27 01:12:14 +0100104 if self.socket is None:
105 return
106
107 if debug_tcp_sockets:
Neels Hofmeyr8e9f30f2017-02-27 01:17:22 +0100108 try:
109 VTYInteract.all_sockets.remove(self.socket)
110 except ValueError:
111 pass
Max6ccd0782017-12-15 12:15:39 +0100112 print("Socket: closing %s:%d %r (%d sockets open)" % (
Neels Hofmeyre3493202017-02-27 01:12:14 +0100113 self.host, self.port, self.socket,
Max6ccd0782017-12-15 12:15:39 +0100114 len(VTYInteract.all_sockets)))
Neels Hofmeyre3493202017-02-27 01:12:14 +0100115 self.socket.close()
116 self.socket = None
Kata7185c62013-04-04 17:31:13 +0200117
118 def _is_end(self, text, ends):
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200119 """
120 >>> vty = VTYInteract('OsmoNAT', 'localhost', 9999)
121 >>> end = [vty.norm_end, vty.priv_end]
122
123 Simple test
124 >>> text1 = 'abc\\r\\nOsmoNAT> '
125 >>> vty._is_end(text1, end)
126 11
127
128 Simple test with the enabled node
129 >>> text2 = 'abc\\r\\nOsmoNAT# '
130 >>> vty._is_end(text2, end)
131 11
132
133 Now the more complicated one
134 >>> text3 = 'abc\\r\\nOsmoNAT(config)# '
135 >>> vty._is_end(text3, end)
136 19
137
138 Now the more complicated one
139 >>> text4 = 'abc\\r\\nOsmoNAT(config-nat)# '
140 >>> vty._is_end(text4, end)
141 23
142
143 Now the more complicated one
144 >>> text5 = 'abc\\r\\nmoo'
145 >>> vty._is_end(text5, end)
146 0
Jacob Erlbeck41b0d302013-08-30 18:28:05 +0200147
148 Check for node name extraction
149 >>> text6 = 'abc\\r\\nOsmoNAT(config-nat)# '
150 >>> vty._is_end(text6, end)
151 23
152 >>> vty.node()
153 'config-nat'
154
155 Check for empty node name extraction
156 >>> text7 = 'abc\\r\\nOsmoNAT# '
157 >>> vty._is_end(text7, end)
158 11
159 >>> vty.node() is None
160 True
161
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200162 """
Jacob Erlbeck41b0d302013-08-30 18:28:05 +0200163 self.last_node = None
Kata7185c62013-04-04 17:31:13 +0200164 for end in ends:
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200165 match = end.search(text)
166 if match:
Jacob Erlbeck41b0d302013-08-30 18:28:05 +0200167 self.last_node = match.group(1)
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200168 return match.end() - match.start()
169 return 0
Kata7185c62013-04-04 17:31:13 +0200170
171 def _common_command(self, request, close=False, ends=None):
Neels Hofmeyrcb320b82017-02-27 01:11:13 +0100172 global debug_tcp_sockets
Kata7185c62013-04-04 17:31:13 +0200173 if not ends:
174 ends = [self.norm_end, self.priv_end]
Neels Hofmeyr4e64b882017-02-27 01:31:02 +0100175
176 self._connect_socket()
Kata7185c62013-04-04 17:31:13 +0200177
178 # Now send the command
179 self.socket.send("%s\r" % request)
180 res = ""
181 end = ""
182
183 # Unfortunately, timeout and recv don't always play nicely
184 while True:
185 data = self.socket.recv(4096)
186 res = "%s%s" % (res, data)
187 if not res: # yes, this is ugly
188 raise IOError("Failed to read data (did the app crash?)")
189 end = self._is_end(res, ends)
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200190 if end > 0:
Kata7185c62013-04-04 17:31:13 +0200191 break
192
193 if close:
194 self._close_socket()
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200195 return res[len(request) + 2: -end]
Kata7185c62013-04-04 17:31:13 +0200196
Alexander Chemeris2f483132015-05-30 10:07:53 -0400197 """A generator function yielding lines separated by delim.
198 Behaves similar to a file readlines() method.
199
200 Example of use:
201 for line in vty.readlines():
202 print line
203 """
204 def readlines(self, recv_buffer=4096, delim='\n'):
205 buffer = ''
206 data = True
207 while data:
208 data = self.socket.recv(recv_buffer)
209 buffer += data
210
211 while buffer.find(delim) != -1:
212 line, buffer = buffer.split('\n', 1)
213 yield line
214 return
215
Kata7185c62013-04-04 17:31:13 +0200216 # There's no close parameter, as close=True makes this useless
217 def enable(self):
218 self.command("enable")
219
220 """Run a command on the vty"""
Kata8ee6bb2013-04-05 17:06:30 +0200221
Kata7185c62013-04-04 17:31:13 +0200222 def command(self, request, close=False):
223 return self._common_command(request, close)
224
225 """Run enable, followed by another command"""
226 def enabled_command(self, request, close=False):
227 self.enable()
228 return self._common_command(request, close)
229
230 """Verify, ignoring leading/trailing whitespace"""
231 # inspired by diff -w, though not identical
232 def w_verify(self, command, results, close=False, loud=True):
233 return self.verify(command, results, close, loud, lambda x: x.strip())
234
235 """Verify that a command has the expected results
236
237 command = the command to verify
238 results = the expected results [line1, line2, ...]
239 close = True to close the socket after running the verify
240 loud = True to show what was expected and what actually happend, stdout
241 f = A function to run over the expected and actual results, before compare
242
243 Returns True iff the expected and actual results match"""
244 def verify(self, command, results, close=False, loud=True, f=None):
245 res = self.command(command, close).split('\r\n')
246 if f:
247 res = map(f, res)
248 results = map(f, results)
249
250 if loud:
251 if res != results:
Max6ccd0782017-12-15 12:15:39 +0100252 print("Rec: %s\nExp: %s" % (res, results))
Kata7185c62013-04-04 17:31:13 +0200253
254 return res == results
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200255
Jacob Erlbeck41b0d302013-08-30 18:28:05 +0200256 def node(self):
257 return self.last_node
258
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200259if __name__ == "__main__":
260 import doctest
261 doctest.testmod()