Holger Hans Peter Freyther | 99bbea7 | 2013-06-25 08:17:59 +0200 | [diff] [blame] | 1 | # Copyright (C) 2012, 2013 Holger Hans Peter Freyther |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 2 | # 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 | # |
Max | 6ccd078 | 2017-12-15 12:15:39 +0100 | [diff] [blame] | 19 | from __future__ import print_function |
Holger Hans Peter Freyther | 99bbea7 | 2013-06-25 08:17:59 +0200 | [diff] [blame] | 20 | import re |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 21 | import socket |
Neels Hofmeyr | 93a808e | 2017-02-24 20:49:21 +0100 | [diff] [blame] | 22 | import sys, subprocess |
Neels Hofmeyr | 2bdab3d | 2017-02-27 00:58:19 +0100 | [diff] [blame] | 23 | import os |
Neels Hofmeyr | abd4b7d | 2017-02-27 01:03:44 +0100 | [diff] [blame] | 24 | import time |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 25 | |
Kat | a8ee6bb | 2013-04-05 17:06:30 +0200 | [diff] [blame] | 26 | """VTYInteract: interact with an osmocom vty |
| 27 | |
| 28 | Specify a VTY to connect to, and run commands on it. |
| 29 | Connections will be reestablished as necessary. |
| 30 | Methods: __init__, command, enabled_command, verify, w_verify""" |
| 31 | |
Neels Hofmeyr | 2bdab3d | 2017-02-27 00:58:19 +0100 | [diff] [blame] | 32 | debug_tcp_sockets = (os.getenv('OSMOPY_DEBUG_TCP_SOCKETS', '0') != '0') |
Neels Hofmeyr | 93a808e | 2017-02-24 20:49:21 +0100 | [diff] [blame] | 33 | |
| 34 | def cmd(what): |
Max | 6ccd078 | 2017-12-15 12:15:39 +0100 | [diff] [blame] | 35 | print('\n> %s' % what) |
Neels Hofmeyr | 93a808e | 2017-02-24 20:49:21 +0100 | [diff] [blame] | 36 | sys.stdout.flush() |
| 37 | subprocess.call(what, shell=True) |
| 38 | sys.stdout.flush() |
| 39 | sys.stderr.flush() |
Max | 6ccd078 | 2017-12-15 12:15:39 +0100 | [diff] [blame] | 40 | print('') |
Neels Hofmeyr | 93a808e | 2017-02-24 20:49:21 +0100 | [diff] [blame] | 41 | sys.stdout.flush() |
| 42 | |
| 43 | def print_used_tcp_sockets(): |
Neels Hofmeyr | cb320b8 | 2017-02-27 01:11:13 +0100 | [diff] [blame] | 44 | global debug_tcp_sockets |
Neels Hofmeyr | 93a808e | 2017-02-24 20:49:21 +0100 | [diff] [blame] | 45 | if not debug_tcp_sockets: |
| 46 | return |
Holger Hans Peter Freyther | f41db1e | 2017-09-13 15:42:15 +0800 | [diff] [blame] | 47 | cmd('ls -l /proc/' + str(os.getpid()) + '/fd'); |
Neels Hofmeyr | 93a808e | 2017-02-24 20:49:21 +0100 | [diff] [blame] | 48 | cmd('ss -tn'); |
| 49 | cmd('ss -tln'); |
| 50 | cmd('ps xua | grep osmo'); |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 51 | |
| 52 | class VTYInteract(object): |
Kat | a8ee6bb | 2013-04-05 17:06:30 +0200 | [diff] [blame] | 53 | """__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 Hofmeyr | 93a808e | 2017-02-24 20:49:21 +0100 | [diff] [blame] | 58 | |
| 59 | all_sockets = [] |
| 60 | |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 61 | def __init__(self, name, host, port): |
Neels Hofmeyr | 93a808e | 2017-02-24 20:49:21 +0100 | [diff] [blame] | 62 | print_used_tcp_sockets() |
| 63 | |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 64 | self.name = name |
| 65 | self.host = host |
| 66 | self.port = port |
| 67 | |
| 68 | self.socket = None |
Jacob Erlbeck | 41b0d30 | 2013-08-30 18:28:05 +0200 | [diff] [blame] | 69 | 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 = '' |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 72 | |
Neels Hofmeyr | 4e64b88 | 2017-02-27 01:31:02 +0100 | [diff] [blame] | 73 | def _connect_socket(self): |
| 74 | if self.socket is not None: |
| 75 | return |
Neels Hofmeyr | abd4b7d | 2017-02-27 01:03:44 +0100 | [diff] [blame] | 76 | 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: |
Max | 6ccd078 | 2017-12-15 12:15:39 +0100 | [diff] [blame] | 90 | print("Connecting socket failed, retrying...") |
Neels Hofmeyr | abd4b7d | 2017-02-27 01:03:44 +0100 | [diff] [blame] | 91 | time.sleep(.1) |
| 92 | continue |
| 93 | break |
| 94 | |
Neels Hofmeyr | 4e64b88 | 2017-02-27 01:31:02 +0100 | [diff] [blame] | 95 | if debug_tcp_sockets: |
| 96 | VTYInteract.all_sockets.append(self.socket) |
Max | 6ccd078 | 2017-12-15 12:15:39 +0100 | [diff] [blame] | 97 | print("Socket: in %d tries, connected to %s:%d %r (%d sockets open)" % ( |
Neels Hofmeyr | abd4b7d | 2017-02-27 01:03:44 +0100 | [diff] [blame] | 98 | took, self.host, self.port, self.socket, |
Max | 6ccd078 | 2017-12-15 12:15:39 +0100 | [diff] [blame] | 99 | len(VTYInteract.all_sockets))) |
Neels Hofmeyr | 4e64b88 | 2017-02-27 01:31:02 +0100 | [diff] [blame] | 100 | self.socket.recv(4096) |
| 101 | |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 102 | def _close_socket(self): |
Neels Hofmeyr | cb320b8 | 2017-02-27 01:11:13 +0100 | [diff] [blame] | 103 | global debug_tcp_sockets |
Neels Hofmeyr | e349320 | 2017-02-27 01:12:14 +0100 | [diff] [blame] | 104 | if self.socket is None: |
| 105 | return |
| 106 | |
| 107 | if debug_tcp_sockets: |
Neels Hofmeyr | 8e9f30f | 2017-02-27 01:17:22 +0100 | [diff] [blame] | 108 | try: |
| 109 | VTYInteract.all_sockets.remove(self.socket) |
| 110 | except ValueError: |
| 111 | pass |
Max | 6ccd078 | 2017-12-15 12:15:39 +0100 | [diff] [blame] | 112 | print("Socket: closing %s:%d %r (%d sockets open)" % ( |
Neels Hofmeyr | e349320 | 2017-02-27 01:12:14 +0100 | [diff] [blame] | 113 | self.host, self.port, self.socket, |
Max | 6ccd078 | 2017-12-15 12:15:39 +0100 | [diff] [blame] | 114 | len(VTYInteract.all_sockets))) |
Neels Hofmeyr | e349320 | 2017-02-27 01:12:14 +0100 | [diff] [blame] | 115 | self.socket.close() |
| 116 | self.socket = None |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 117 | |
| 118 | def _is_end(self, text, ends): |
Holger Hans Peter Freyther | 99bbea7 | 2013-06-25 08:17:59 +0200 | [diff] [blame] | 119 | """ |
| 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 Erlbeck | 41b0d30 | 2013-08-30 18:28:05 +0200 | [diff] [blame] | 147 | |
| 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 Freyther | 99bbea7 | 2013-06-25 08:17:59 +0200 | [diff] [blame] | 162 | """ |
Jacob Erlbeck | 41b0d30 | 2013-08-30 18:28:05 +0200 | [diff] [blame] | 163 | self.last_node = None |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 164 | for end in ends: |
Holger Hans Peter Freyther | 99bbea7 | 2013-06-25 08:17:59 +0200 | [diff] [blame] | 165 | match = end.search(text) |
| 166 | if match: |
Jacob Erlbeck | 41b0d30 | 2013-08-30 18:28:05 +0200 | [diff] [blame] | 167 | self.last_node = match.group(1) |
Holger Hans Peter Freyther | 99bbea7 | 2013-06-25 08:17:59 +0200 | [diff] [blame] | 168 | return match.end() - match.start() |
| 169 | return 0 |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 170 | |
| 171 | def _common_command(self, request, close=False, ends=None): |
Neels Hofmeyr | cb320b8 | 2017-02-27 01:11:13 +0100 | [diff] [blame] | 172 | global debug_tcp_sockets |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 173 | if not ends: |
| 174 | ends = [self.norm_end, self.priv_end] |
Neels Hofmeyr | 4e64b88 | 2017-02-27 01:31:02 +0100 | [diff] [blame] | 175 | |
| 176 | self._connect_socket() |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 177 | |
| 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 Freyther | 99bbea7 | 2013-06-25 08:17:59 +0200 | [diff] [blame] | 190 | if end > 0: |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 191 | break |
| 192 | |
| 193 | if close: |
| 194 | self._close_socket() |
Holger Hans Peter Freyther | 99bbea7 | 2013-06-25 08:17:59 +0200 | [diff] [blame] | 195 | return res[len(request) + 2: -end] |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 196 | |
Alexander Chemeris | 2f48313 | 2015-05-30 10:07:53 -0400 | [diff] [blame] | 197 | """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 | |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 216 | # 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""" |
Kat | a8ee6bb | 2013-04-05 17:06:30 +0200 | [diff] [blame] | 221 | |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 222 | 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: |
Max | 6ccd078 | 2017-12-15 12:15:39 +0100 | [diff] [blame] | 252 | print("Rec: %s\nExp: %s" % (res, results)) |
Kat | a7185c6 | 2013-04-04 17:31:13 +0200 | [diff] [blame] | 253 | |
| 254 | return res == results |
Holger Hans Peter Freyther | 99bbea7 | 2013-06-25 08:17:59 +0200 | [diff] [blame] | 255 | |
Jacob Erlbeck | 41b0d30 | 2013-08-30 18:28:05 +0200 | [diff] [blame] | 256 | def node(self): |
| 257 | return self.last_node |
| 258 | |
Holger Hans Peter Freyther | 99bbea7 | 2013-06-25 08:17:59 +0200 | [diff] [blame] | 259 | if __name__ == "__main__": |
| 260 | import doctest |
| 261 | doctest.testmod() |