blob: e66ed22cca71e4903d6ec4b4524720b52c4b139b [file] [log] [blame]
Neels Hofmeyr56aa4782017-12-19 14:12:16 +01001#!/usr/bin/env python3
2#
3# (C) 2017 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
4# All rights reserved.
5#
6# Author: Neels Hofmeyr <nhofmeyr@sysmocom.de>
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21'''
22Run VTY commands or test transcripts against a given application. Commandline
23invocation exposes only direct command piping, the transcript verification code
24is exposed as commandline args by osmo_verify_transcript_vty.py.
25'''
26
27import re
28
29from .common import *
30
31class InteractVty(Interact):
32
33 class VtyStep(Interact.StepBase):
34 expect_node = None # e.g. '(config-net)'
35 expect_prompt_char = None # '>' or '#'
36
37 def __init__(self, prompt):
38 super().__init__()
39 self.prompt = prompt
40
41 def verify_interact_state(self, interact_instance):
42 if interact_instance.update:
43 return
44 if interact_instance.this_node != self.expect_node:
45 raise Exception('Mismatch: expected VTY node %r in the prompt, got %r'
46 % (self.expect_node, interact_instance.this_node))
47 if interact_instance.this_prompt_char != self.expect_prompt_char:
48 raise Exception('Mismatch: expected VTY prompt character %r, got %r'
49 % (self.expect_prompt_char, interact_instance.this_prompt_char))
50
51 @staticmethod
52 def is_next_step(line, interact_instance):
53 m = interact_instance.re_prompt.match(line)
54 if not m:
55 return None
56 next_step = InteractVty.VtyStep(interact_instance.prompt)
57 next_step.expect_node = m.group(1)
58 next_step.expect_prompt_char = m.group(2)
59 next_step.command = m.group(3)
60 return next_step
61
62 def command_str(self, interact_instance=None):
63 if interact_instance is None:
64 node = self.expect_node
65 prompt_char = self.expect_prompt_char
66 else:
67 node = interact_instance.last_node
68 prompt_char = interact_instance.last_prompt_char
69 if node:
70 node = '(%s)' % node
71 node = node or ''
72 return '%s%s%s %s' % (self.prompt, node, prompt_char, self.command)
73
74 def __init__(self, prompt, port, host, verbose, update):
75 self.prompt = prompt
76 super().__init__(InteractVty.VtyStep, port, host, verbose, update)
77
78 def connect(self):
79 self.this_node = None
80 self.this_prompt_char = '>' # slight cheat for initial prompt char
81 self.last_node = None
82 self.last_prompt_char = None
83
84 super().connect()
85 # receive the first welcome message and discard
86 data = self.socket.recv(4096)
87 if not self.prompt:
88 b = data
89 b = b[b.rfind(b'\n') + 1:]
90 while b and (b[0] < ord('A') or b[0] > ord('z')):
91 b = b[1:]
92 prompt_str = b.decode('utf-8')
93 if '>' in prompt_str:
94 self.prompt = prompt_str[:prompt_str.find('>')]
95 if not self.prompt:
96 raise Exception('Could not find application name; needed to decode prompts.'
97 ' Initial data was: %r' % data)
98 self.re_prompt = re.compile('^%s(?:\(([\w-]*)\))?([#>]) (.*)$' % self.prompt)
99
100 def _command(self, command_str, timeout=10):
101 self.socket.send(command_str.encode())
102
103 waited_since = time.time()
104 received_lines = []
105 last_line = ''
106
107 while True:
108 new_data = self.socket.recv(4096).decode('utf-8')
109
110 last_line = "%s%s" % (last_line, new_data)
111
112 if last_line:
Neels Hofmeyrb0621452018-09-10 15:44:45 +0200113 # Separate the received response into lines.
114 # But note: the VTY logging currently separates with '\n\r', not '\r\n',
115 # see _vty_output() in libosmocore logging_vty.c.
116 # So we need to jump through hoops to not separate 'abc\n\rdef' as
117 # [ 'abc', '', 'def' ]; but also not to convert '\r\n\r\n' to '\r\n\n' ('\r{\r\n}\n')
118 # Simplest is to just drop all the '\r' and only care about the '\n'.
119 lines = last_line.replace('\r', '').splitlines()
Neels Hofmeyr56aa4782017-12-19 14:12:16 +0100120 received_lines.extend(lines[:-1])
121 last_line = lines[-1]
122
123 match = self.re_prompt.match(last_line)
124 if not match:
125 if time.time() - waited_since > timeout:
126 raise IOError("Failed to read data (did the app crash?)")
127 time.sleep(.1)
128 continue
129
130 self.last_node = self.this_node
131 self.last_prompt_char = self.this_prompt_char
132 self.this_node = match.group(1) or None
133 self.this_prompt_char = match.group(2)
134 break
135
136 # expecting to have received the command we sent as echo, remove it
137 clean_command_str = command_str.strip()
138 if clean_command_str.endswith('?'):
139 clean_command_str = clean_command_str[:-1]
140 if received_lines and received_lines[0] == clean_command_str:
141 received_lines = received_lines[1:]
142 return received_lines
143
144 def command(self, command_str, timeout=10):
145 command_str = command_str or '\r'
146 if command_str[-1] not in '?\r\t':
147 command_str = command_str + '\r'
148
149 received_lines = self._command(command_str, timeout)
150
151 # send escape to cancel the '?' command line
152 if command_str[-1] == '?':
153 self._command('\x03', timeout)
154
155 return received_lines
156
157def parser_add_vty_args(parser):
158 parser.add_argument('-n', '--prompt-name', dest='prompt',
159 help="Name used in application's telnet VTY prompt."
160 " If omitted, will attempt to determine the name from"
161 " the initial VTY prompt.")
162 return parser
163
164def main_interact_vty():
Neels Hofmeyrbe76f4d2017-12-19 13:46:57 +0100165 '''
166Run VTY commands against a given application by stdin/stdout piping.
167
168Optionally, this can launch and tear down the application with -r.
169
170For example, to extract the VTY reference XML file from osmo-hlr:
171
172 osmo_interact_vty.py -p 4258 --gen-xml-ref \\
173 -r 'osmo-hlr -c /etc/osmocom/osmo-hlr.cfg -l /tmp/hlr.db'
174
175Where 4258 is OsmoHLR's VTY port number, see
176https://osmocom.org/projects/cellular-infrastructure/wiki/Port_Numbers
177
178If osmo-hlr is already running, this shortens to just
179
180 osmo_interact_vty.py -p 4258 --gen-xml-ref
181
182See also osmo_verify_transcript_vty.py, which allows verifying and updating
183complete VTY session transcripts, in essence to write VTY tests from a screen
184dump of a VTY session.
185
186A Control interface equivalent is osmo_interact_ctrl.py.
187'''
188 parser = common_parser(__doc__)
Neels Hofmeyr56aa4782017-12-19 14:12:16 +0100189 parser_add_vty_args(parser)
190 parser_add_run_args(parser)
191 parser.add_argument('-X', '--gen-xml-ref', dest='gen_xml', action='store_true',
192 help="Equivalent to '-c \"show online-help\" -O -',"
193 " can be used to generate the VTY reference file as"
194 " required by osmo-gsm-manuals.git.")
195 args = parser.parse_args()
196
197 if args.gen_xml:
198 if args.cmd_str:
199 raise Exception('It does not make sense to pass both --command and'
200 ' --gen-xml-ref.')
201 args.cmd_str = 'show online-help'
202
203 interact = InteractVty(args.prompt, args.port, args.host,
204 verbose=False, update=False)
205
206 main_run_commands(args.run_app_str, args.output_path, args.cmd_str,
207 args.cmd_files, interact)
208
209def main_verify_transcript_vty():
Neels Hofmeyrbe76f4d2017-12-19 13:46:57 +0100210 '''
211A VTY transcript contains VTY commands and their expected results.
212It looks like a screen dump of a live VTY session:
213
214"
215OsmoHLR> enable
216
217OsmoHLR# subscriber show imsi 123456789023000
218% No subscriber for imsi = '123456789023000'
219OsmoHLR# subscriber show msisdn 12345
220% No subscriber for msisdn = '12345'
221
222OsmoHLR# subscriber create imsi 123456789023000
223% Created subscriber 123456789023000
224 ID: 1
225 IMSI: 123456789023000
226 MSISDN: none
227 No auth data
228"
229
230Optionally, this can launch and tear down the application with -r.
231
232For example, if above transcript example is in file test.vty, you can verify
233that OsmoHLR still shows this behavior by:
234
235 osmo_interact_vty.py -p 4258 \\
236 -r 'osmo-hlr -c /etc/osmocom/osmo-hlr.cfg -l /tmp/hlr.db' \\
237 test.vty
238
239Where 4258 is OsmoHLR's VTY port number, see
240https://osmocom.org/projects/cellular-infrastructure/wiki/Port_Numbers
241
242If osmo-hlr is already running, this shortens to just
243
244 osmo_interact_vty.py -p 4258 test.vty
245
246If osmo-hlr has changed its behavior, e.g. some error message changed, the
247transcript can be automatically updated, which overwrites the file, like:
248
249 osmo_interact_vty.py -p 4258 -u test.vty
250
251See also osmo_interact_vty.py, which allows piping VTY commands to stdin.
252
253A Control interface equivalent is osmo_verify_transcript_ctrl.py.
254'''
255 parser = common_parser(__doc__)
Neels Hofmeyr56aa4782017-12-19 14:12:16 +0100256 parser_add_vty_args(parser)
257 parser_add_verify_args(parser)
258 args = parser.parse_args()
259
260 interact = InteractVty(args.prompt, args.port, args.host, args.verbose, args.update)
261
262 main_verify_transcripts(args.run_app_str, args.transcript_files, interact, args.verbose)
263
264# vim: tabstop=4 shiftwidth=4 expandtab nocin ai