blob: f614518f4f9d68ff4963510b6f1af55f73d7c908 [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
Kata7185c62013-04-04 17:31:13 +020023
Kata8ee6bb2013-04-05 17:06:30 +020024"""VTYInteract: interact with an osmocom vty
25
26Specify a VTY to connect to, and run commands on it.
27Connections will be reestablished as necessary.
28Methods: __init__, command, enabled_command, verify, w_verify"""
29
Neels Hofmeyr2bdab3d2017-02-27 00:58:19 +010030debug_tcp_sockets = (os.getenv('OSMOPY_DEBUG_TCP_SOCKETS', '0') != '0')
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010031
32def cmd(what):
33 print '\n> %s' % what
34 sys.stdout.flush()
35 subprocess.call(what, shell=True)
36 sys.stdout.flush()
37 sys.stderr.flush()
38 print ''
39 sys.stdout.flush()
40
41def print_used_tcp_sockets():
Neels Hofmeyrcb320b82017-02-27 01:11:13 +010042 global debug_tcp_sockets
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010043 if not debug_tcp_sockets:
44 return
Neels Hofmeyrac0b59d2017-02-27 01:02:45 +010045 cmd('ls /proc/self/fd');
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010046 cmd('ss -tn');
47 cmd('ss -tln');
48 cmd('ps xua | grep osmo');
Kata7185c62013-04-04 17:31:13 +020049
50class VTYInteract(object):
Kata8ee6bb2013-04-05 17:06:30 +020051 """__init__(self, name, host, port):
52
53 name is the name the vty prints for commands, ie OpenBSC
54 host is the hostname to connect to
55 port is the port to connect on"""
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010056
57 all_sockets = []
58
Kata7185c62013-04-04 17:31:13 +020059 def __init__(self, name, host, port):
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010060 print_used_tcp_sockets()
61
Kata7185c62013-04-04 17:31:13 +020062 self.name = name
63 self.host = host
64 self.port = port
65
66 self.socket = None
Jacob Erlbeck41b0d302013-08-30 18:28:05 +020067 self.norm_end = re.compile('\r\n%s(?:\(([\w-]*)\))?> $' % self.name)
68 self.priv_end = re.compile('\r\n%s(?:\(([\w-]*)\))?# $' % self.name)
69 self.last_node = ''
Kata7185c62013-04-04 17:31:13 +020070
71 def _close_socket(self):
Neels Hofmeyrcb320b82017-02-27 01:11:13 +010072 global debug_tcp_sockets
Holger Hans Peter Freyther99b5c562017-02-13 20:06:44 +070073 if self.socket:
Neels Hofmeyr93a808e2017-02-24 20:49:21 +010074 if debug_tcp_sockets:
75 VTYInteract.all_sockets.remove(self.socket)
76 print "Socket: closing %s:%d %r (%d sockets open)" % (
77 self.host, self.port, self.socket,
78 len(VTYInteract.all_sockets))
Holger Hans Peter Freyther99b5c562017-02-13 20:06:44 +070079 self.socket.close()
80 self.socket = None
Kata7185c62013-04-04 17:31:13 +020081
82 def _is_end(self, text, ends):
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +020083 """
84 >>> vty = VTYInteract('OsmoNAT', 'localhost', 9999)
85 >>> end = [vty.norm_end, vty.priv_end]
86
87 Simple test
88 >>> text1 = 'abc\\r\\nOsmoNAT> '
89 >>> vty._is_end(text1, end)
90 11
91
92 Simple test with the enabled node
93 >>> text2 = 'abc\\r\\nOsmoNAT# '
94 >>> vty._is_end(text2, end)
95 11
96
97 Now the more complicated one
98 >>> text3 = 'abc\\r\\nOsmoNAT(config)# '
99 >>> vty._is_end(text3, end)
100 19
101
102 Now the more complicated one
103 >>> text4 = 'abc\\r\\nOsmoNAT(config-nat)# '
104 >>> vty._is_end(text4, end)
105 23
106
107 Now the more complicated one
108 >>> text5 = 'abc\\r\\nmoo'
109 >>> vty._is_end(text5, end)
110 0
Jacob Erlbeck41b0d302013-08-30 18:28:05 +0200111
112 Check for node name extraction
113 >>> text6 = 'abc\\r\\nOsmoNAT(config-nat)# '
114 >>> vty._is_end(text6, end)
115 23
116 >>> vty.node()
117 'config-nat'
118
119 Check for empty node name extraction
120 >>> text7 = 'abc\\r\\nOsmoNAT# '
121 >>> vty._is_end(text7, end)
122 11
123 >>> vty.node() is None
124 True
125
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200126 """
Jacob Erlbeck41b0d302013-08-30 18:28:05 +0200127 self.last_node = None
Kata7185c62013-04-04 17:31:13 +0200128 for end in ends:
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200129 match = end.search(text)
130 if match:
Jacob Erlbeck41b0d302013-08-30 18:28:05 +0200131 self.last_node = match.group(1)
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200132 return match.end() - match.start()
133 return 0
Kata7185c62013-04-04 17:31:13 +0200134
135 def _common_command(self, request, close=False, ends=None):
Neels Hofmeyrcb320b82017-02-27 01:11:13 +0100136 global debug_tcp_sockets
Kata7185c62013-04-04 17:31:13 +0200137 if not ends:
138 ends = [self.norm_end, self.priv_end]
139 if not self.socket:
140 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
141 self.socket.setblocking(1)
142 self.socket.connect((self.host, self.port))
Neels Hofmeyr93a808e2017-02-24 20:49:21 +0100143 if debug_tcp_sockets:
144 VTYInteract.all_sockets.append(self.socket)
145 print "Socket: connected to %s:%d %r (%d sockets open)" % (
146 self.host, self.port, self.socket,
147 len(VTYInteract.all_sockets))
Kata7185c62013-04-04 17:31:13 +0200148 self.socket.recv(4096)
149
150 # Now send the command
151 self.socket.send("%s\r" % request)
152 res = ""
153 end = ""
154
155 # Unfortunately, timeout and recv don't always play nicely
156 while True:
157 data = self.socket.recv(4096)
158 res = "%s%s" % (res, data)
159 if not res: # yes, this is ugly
160 raise IOError("Failed to read data (did the app crash?)")
161 end = self._is_end(res, ends)
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200162 if end > 0:
Kata7185c62013-04-04 17:31:13 +0200163 break
164
165 if close:
166 self._close_socket()
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200167 return res[len(request) + 2: -end]
Kata7185c62013-04-04 17:31:13 +0200168
Alexander Chemeris2f483132015-05-30 10:07:53 -0400169 """A generator function yielding lines separated by delim.
170 Behaves similar to a file readlines() method.
171
172 Example of use:
173 for line in vty.readlines():
174 print line
175 """
176 def readlines(self, recv_buffer=4096, delim='\n'):
177 buffer = ''
178 data = True
179 while data:
180 data = self.socket.recv(recv_buffer)
181 buffer += data
182
183 while buffer.find(delim) != -1:
184 line, buffer = buffer.split('\n', 1)
185 yield line
186 return
187
Kata7185c62013-04-04 17:31:13 +0200188 # There's no close parameter, as close=True makes this useless
189 def enable(self):
190 self.command("enable")
191
192 """Run a command on the vty"""
Kata8ee6bb2013-04-05 17:06:30 +0200193
Kata7185c62013-04-04 17:31:13 +0200194 def command(self, request, close=False):
195 return self._common_command(request, close)
196
197 """Run enable, followed by another command"""
198 def enabled_command(self, request, close=False):
199 self.enable()
200 return self._common_command(request, close)
201
202 """Verify, ignoring leading/trailing whitespace"""
203 # inspired by diff -w, though not identical
204 def w_verify(self, command, results, close=False, loud=True):
205 return self.verify(command, results, close, loud, lambda x: x.strip())
206
207 """Verify that a command has the expected results
208
209 command = the command to verify
210 results = the expected results [line1, line2, ...]
211 close = True to close the socket after running the verify
212 loud = True to show what was expected and what actually happend, stdout
213 f = A function to run over the expected and actual results, before compare
214
215 Returns True iff the expected and actual results match"""
216 def verify(self, command, results, close=False, loud=True, f=None):
217 res = self.command(command, close).split('\r\n')
218 if f:
219 res = map(f, res)
220 results = map(f, results)
221
222 if loud:
223 if res != results:
224 print "Rec: %s\nExp: %s" % (res, results)
225
226 return res == results
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200227
Jacob Erlbeck41b0d302013-08-30 18:28:05 +0200228 def node(self):
229 return self.last_node
230
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200231if __name__ == "__main__":
232 import doctest
233 doctest.testmod()