blob: 3c9edb1051e08699ec8466205dacc8cf034df3a3 [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
21
Kata8ee6bb2013-04-05 17:06:30 +020022"""VTYInteract: interact with an osmocom vty
23
24Specify a VTY to connect to, and run commands on it.
25Connections will be reestablished as necessary.
26Methods: __init__, command, enabled_command, verify, w_verify"""
27
Kata7185c62013-04-04 17:31:13 +020028
29class VTYInteract(object):
Kata8ee6bb2013-04-05 17:06:30 +020030 """__init__(self, name, host, port):
31
32 name is the name the vty prints for commands, ie OpenBSC
33 host is the hostname to connect to
34 port is the port to connect on"""
Kata7185c62013-04-04 17:31:13 +020035 def __init__(self, name, host, port):
36 self.name = name
37 self.host = host
38 self.port = port
39
40 self.socket = None
Jacob Erlbeck41b0d302013-08-30 18:28:05 +020041 self.norm_end = re.compile('\r\n%s(?:\(([\w-]*)\))?> $' % self.name)
42 self.priv_end = re.compile('\r\n%s(?:\(([\w-]*)\))?# $' % self.name)
43 self.last_node = ''
Kata7185c62013-04-04 17:31:13 +020044
45 def _close_socket(self):
Holger Hans Peter Freyther99b5c562017-02-13 20:06:44 +070046 if self.socket:
47 self.socket.close()
48 self.socket = None
Kata7185c62013-04-04 17:31:13 +020049
50 def _is_end(self, text, ends):
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +020051 """
52 >>> vty = VTYInteract('OsmoNAT', 'localhost', 9999)
53 >>> end = [vty.norm_end, vty.priv_end]
54
55 Simple test
56 >>> text1 = 'abc\\r\\nOsmoNAT> '
57 >>> vty._is_end(text1, end)
58 11
59
60 Simple test with the enabled node
61 >>> text2 = 'abc\\r\\nOsmoNAT# '
62 >>> vty._is_end(text2, end)
63 11
64
65 Now the more complicated one
66 >>> text3 = 'abc\\r\\nOsmoNAT(config)# '
67 >>> vty._is_end(text3, end)
68 19
69
70 Now the more complicated one
71 >>> text4 = 'abc\\r\\nOsmoNAT(config-nat)# '
72 >>> vty._is_end(text4, end)
73 23
74
75 Now the more complicated one
76 >>> text5 = 'abc\\r\\nmoo'
77 >>> vty._is_end(text5, end)
78 0
Jacob Erlbeck41b0d302013-08-30 18:28:05 +020079
80 Check for node name extraction
81 >>> text6 = 'abc\\r\\nOsmoNAT(config-nat)# '
82 >>> vty._is_end(text6, end)
83 23
84 >>> vty.node()
85 'config-nat'
86
87 Check for empty node name extraction
88 >>> text7 = 'abc\\r\\nOsmoNAT# '
89 >>> vty._is_end(text7, end)
90 11
91 >>> vty.node() is None
92 True
93
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +020094 """
Jacob Erlbeck41b0d302013-08-30 18:28:05 +020095 self.last_node = None
Kata7185c62013-04-04 17:31:13 +020096 for end in ends:
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +020097 match = end.search(text)
98 if match:
Jacob Erlbeck41b0d302013-08-30 18:28:05 +020099 self.last_node = match.group(1)
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200100 return match.end() - match.start()
101 return 0
Kata7185c62013-04-04 17:31:13 +0200102
103 def _common_command(self, request, close=False, ends=None):
104 if not ends:
105 ends = [self.norm_end, self.priv_end]
106 if not self.socket:
107 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
108 self.socket.setblocking(1)
109 self.socket.connect((self.host, self.port))
110 self.socket.recv(4096)
111
112 # Now send the command
113 self.socket.send("%s\r" % request)
114 res = ""
115 end = ""
116
117 # Unfortunately, timeout and recv don't always play nicely
118 while True:
119 data = self.socket.recv(4096)
120 res = "%s%s" % (res, data)
121 if not res: # yes, this is ugly
122 raise IOError("Failed to read data (did the app crash?)")
123 end = self._is_end(res, ends)
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200124 if end > 0:
Kata7185c62013-04-04 17:31:13 +0200125 break
126
127 if close:
128 self._close_socket()
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200129 return res[len(request) + 2: -end]
Kata7185c62013-04-04 17:31:13 +0200130
Alexander Chemeris2f483132015-05-30 10:07:53 -0400131 """A generator function yielding lines separated by delim.
132 Behaves similar to a file readlines() method.
133
134 Example of use:
135 for line in vty.readlines():
136 print line
137 """
138 def readlines(self, recv_buffer=4096, delim='\n'):
139 buffer = ''
140 data = True
141 while data:
142 data = self.socket.recv(recv_buffer)
143 buffer += data
144
145 while buffer.find(delim) != -1:
146 line, buffer = buffer.split('\n', 1)
147 yield line
148 return
149
Kata7185c62013-04-04 17:31:13 +0200150 # There's no close parameter, as close=True makes this useless
151 def enable(self):
152 self.command("enable")
153
154 """Run a command on the vty"""
Kata8ee6bb2013-04-05 17:06:30 +0200155
Kata7185c62013-04-04 17:31:13 +0200156 def command(self, request, close=False):
157 return self._common_command(request, close)
158
159 """Run enable, followed by another command"""
160 def enabled_command(self, request, close=False):
161 self.enable()
162 return self._common_command(request, close)
163
164 """Verify, ignoring leading/trailing whitespace"""
165 # inspired by diff -w, though not identical
166 def w_verify(self, command, results, close=False, loud=True):
167 return self.verify(command, results, close, loud, lambda x: x.strip())
168
169 """Verify that a command has the expected results
170
171 command = the command to verify
172 results = the expected results [line1, line2, ...]
173 close = True to close the socket after running the verify
174 loud = True to show what was expected and what actually happend, stdout
175 f = A function to run over the expected and actual results, before compare
176
177 Returns True iff the expected and actual results match"""
178 def verify(self, command, results, close=False, loud=True, f=None):
179 res = self.command(command, close).split('\r\n')
180 if f:
181 res = map(f, res)
182 results = map(f, results)
183
184 if loud:
185 if res != results:
186 print "Rec: %s\nExp: %s" % (res, results)
187
188 return res == results
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200189
Jacob Erlbeck41b0d302013-08-30 18:28:05 +0200190 def node(self):
191 return self.last_node
192
Holger Hans Peter Freyther99bbea72013-06-25 08:17:59 +0200193if __name__ == "__main__":
194 import doctest
195 doctest.testmod()