51d703425aa03a00a4d1cb9f05f14a0fc702660b
[pingcheck] / test / connd_state.py
1 #!/usr/bin/env python
2
3 """ Representation for connd state as returned by tell-connd --status
4
5 Christian Herdtweck, Intra2net, January 2015
6 (c) Intra2net AG 2015
7 """
8
9 from __future__ import print_function
10 import subprocess
11 from re import match as regexp
12 from os import EX_OK
13
14 # constants
15 DEFAULT_TELL_CONND_BINARY = '/usr/intranator/bin/tell-connd'
16 TIMEOUT = 1
17
18 ONLINE_STATE_ALWAYS_ONLINE = 'always online'
19 ONLINE_STATE_ALWAYS_OFFLINE = 'always offline'
20 ONLINE_STATE_DIAL_ON_COMMAND = 'dial on command'
21 ONLINE_STATE_DIAL_ON_DEMAND = 'dial on demand'
22
23 SUBSYS_DNS = 'dns'
24 SUBSYS_DYNDNS = 'dyndns'
25 SUBSYS_MAIL = 'mail'
26 SUBSYS_NTP = 'ntp'
27 SUBSYS_SOCKS = 'socks'
28 SUBSYS_VPN = 'vpn'
29 SUBSYS_WEBPROXY = 'webproxy'
30 SUBSYS_PINGCHECK = 'pingcheck'
31 SUBSYS_IPONLINE = 'iponline'
32 ALL_SUBSYS = (SUBSYS_DNS, SUBSYS_DYNDNS, SUBSYS_MAIL, SUBSYS_NTP,
33               SUBSYS_SOCKS, SUBSYS_VPN, SUBSYS_WEBPROXY, SUBSYS_PINGCHECK,
34               SUBSYS_IPONLINE)
35
36 ALL_STATES = (ONLINE_STATE_DIAL_ON_DEMAND, ONLINE_STATE_DIAL_ON_COMMAND,
37               ONLINE_STATE_ALWAYS_OFFLINE, ONLINE_STATE_ALWAYS_ONLINE)
38
39
40 class ConndState(object):
41     """ representation of connd's status as returned by tell-connd --status """
42
43     online_mode = None
44     default_provider = None
45     subsys_online = None
46     subsys_offline = None
47     subsys_disabled = None
48     connections = None
49     actions = None
50     online_ips = None
51     connected_vpns = None
52     log_level = None
53     log_file = None
54
55     def __str__(self):
56         return \
57             '[ConndState: {0} (default {1}), {2} conn\'s, {3} ips, {4} vpns ]'\
58             .format(self.online_mode, self.default_provider,
59                     len(self.connections), len(self.online_ips),
60                     len(self.connected_vpns))
61
62     def complete_str(self):
63         """ return a string representating the complete state """
64
65         # general
66         parts = [
67             'ConndState: online mode = "{0}" (default provider: {1})\n'
68             .format(self.online_mode, self.default_provider), ]
69
70         # subsys
71         #            '  connctns:   (repeated here for correct aligning)
72         parts.append('    subsys: online: ')
73         if self.subsys_online:
74             for subsys in self.subsys_online:
75                 parts.append(subsys + ' ')
76         else:
77             parts.append('None ')
78         parts.append(';  offline: ')
79         if self.subsys_offline:
80             for subsys in self.subsys_offline:
81                 parts.append(subsys + ' ')
82         else:
83             parts.append('None ')
84         parts.append(';  disabled: ')
85         if self.subsys_disabled:
86             for subsys in self.subsys_disabled:
87                 parts.append(subsys + ' ')
88         else:
89             parts.append('None')
90         parts.append('\n')
91
92         # connections
93         parts.append('     conns: ')
94         if self.connections:
95             name, info, actions = self.connections[0]
96             parts.append('{0}: {1}, {2}\n'.format(name, info, actions))
97         else:
98             parts.append('None\n')
99         for name, info, actions in self.connections[1:]:
100             #            '  connctns:   (repeated here for correct aligning)
101             parts.append('            {0}: {1}, {2}\n'.format(name, info,
102                                                               actions))
103
104         # actions
105         #            '  connctns:   (repeated here for correct aligning)
106         parts.append('   actions: ')
107         if self.actions:
108             parts.append(self.actions[0] + '\n')
109         else:
110             parts.append('None\n')
111         for action in self.actions[1:]:
112             #            '  connctns:   (repeated here for correct aligning)
113             parts.append('            {0}\n'.format(action))
114
115         # online IPs
116         #            '  connctns:   (repeated here for correct aligning)
117         parts.append('       IPs: ')
118         if self.online_ips:
119             parts.append(self.online_ips[0])
120             for curr_ip in self.online_ips[1:]:
121                 parts.append(', {0}'.format(curr_ip))
122         else:
123             parts.append('None')
124         parts.append('\n')
125
126         # VPNs
127         #            '  connctns:   (repeated here for correct aligning)
128         parts.append('      VPNs: ')
129         if self.connected_vpns:
130             parts.append(self.connected_vpns[0])
131             for vpn in self.connected_vpns[1:]:
132                 parts.append(', {0}'.format(vpn))
133         else:
134             parts.append('None')
135         parts.append('\n')
136
137         # log level and target:
138         #            '  connctns:   (repeated here for correct aligning)
139         parts.append('       Log: level {0}'.format(self.log_level))
140         if self.log_file:
141             parts.append(' to {0}'.format(self.log_file))
142         parts.append('\n')
143
144         return ''.join(parts)
145     # end: ConndState.complete_str
146
147     @staticmethod
148     def run_tell_connd(tell_connd_binary=DEFAULT_TELL_CONND_BINARY):
149         """ run tell-connd --status, return output iterator and return code
150
151         catches all it can, so should usually return (output, return_code)
152           where output = [line1, line2, ...]
153         if return_code != 0, output's first line(s) is error message
154         """
155         try:
156             output = subprocess.check_output(
157                 [tell_connd_binary, '--status'], stderr=subprocess.STDOUT,
158                 universal_newlines=True, shell=False, timeout=TIMEOUT)
159             return EX_OK, output.splitlines()
160         except subprocess.CalledProcessError as cpe:  # non-zero return status
161             output = [
162                 'tell-connd exited with status {0}'.format(cpe.returncode), ]
163             output.extend(cpe.output.splitlines())
164             return cpe.returncode, output
165         # not python-2-compatible:
166         # except subprocess.TimeoutExpired as texp:
167         #     output = [
168         #         'tell-connd timed out after {0}s. Returning -1'.format(
169         #             texp.timeout), ]
170         #     output.extend(te.output.splitlines())
171         #     return -1, output
172         except Exception as exp:
173             output = [str(exp), ]
174             return -1, output
175     # end: ConndState.run_tell_connd
176
177     @staticmethod
178     def get_state(tell_connd_binary=DEFAULT_TELL_CONND_BINARY):
179         """ get actual state from tell-connd --status
180
181         returns (err_code, output_lines) if something goes wrong running
182           binary; raises assertion if output from tell-connd does not match
183           expected format
184         """
185
186         state = ConndState()
187
188         err_code, all_lines = ConndState.run_tell_connd(tell_connd_binary)
189         if err_code != EX_OK:
190             return err_code, all_lines
191
192         output = iter(all_lines)
193
194         # first section
195         line = next(output).strip()
196         state.online_mode = regexp('online mode\s*:\s*(.+)$', line).groups()[0]
197         assert state.online_mode in ALL_STATES, \
198             'unexpected online mode: {0}'.format(state.online_mode)
199
200         line = next(output).strip()
201         state.default_provider = regexp('default provider\s*:\s*(.*)$',
202                                         line).groups()[0]
203         if len(state.default_provider) == 0:
204             state.default_provider = None
205         line = next(output).strip()
206         assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
207
208         # subsys
209         line = next(output).strip()
210         assert line == 'subsys', 'expected subsys but got {0}'.format(line)
211         line = next(output).strip()
212         state.subsys_online = regexp('online\s*:\s*(.*)$', line)\
213             .groups()[0].split()
214         for subsys in state.subsys_online:
215             assert subsys in ALL_SUBSYS, \
216                 'unexpected subsys: {0}'.format(subsys)
217         line = next(output).strip()
218         state.subsys_offline = regexp('offline\s*:\s*(.*)$', line)\
219             .groups()[0].split()
220         for subsys in state.subsys_offline:
221             assert subsys in ALL_SUBSYS, \
222                 'unexpected subsys: {0}'.format(subsys)
223         line = next(output).strip()
224         state.subsys_disabled = regexp('disabled\s*:\s*(.*)$', line)\
225             .groups()[0].split()
226         for subsys in state.subsys_disabled:
227             assert subsys in ALL_SUBSYS, \
228                 'unexpected subsys: {0}'.format(subsys)
229         line = next(output).strip()
230         assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
231
232         # connection map
233         state.connections = []
234         line = next(output).strip()
235         assert line == 'connection map:', \
236             'expected connection map but got {0}'.format(line)
237         expect_new = True
238         for line in output:
239             line = line.strip()
240             if len(line) == 0:
241                 continue
242             if expect_new:
243                 if line == 'end of connection map':
244                     break
245                 conn_name, conn_info = regexp(
246                     '\[\s*(.+)\s*\]\s*:\s*\(\s*(.*)\s*\)', line).groups()
247                 expect_new = False
248             else:
249                 conn_actions = regexp('actions\s*:\s*\[\s*(.+)\s*\]', line)\
250                     .groups()
251                 state.connections.append((conn_name, conn_info, conn_actions))
252                 expect_new = True
253         # end: for lines
254         assert expect_new
255         line = next(output).strip()
256         assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
257
258         # actions
259         line = next(output).strip()
260         state.actions = regexp('actions\s*:\s*(.*)', line).groups()[0].split()
261         if len(state.actions) == 1 and state.actions[0].strip() == '-':
262             state.actions = []
263         line = next(output).strip()
264         assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
265
266         # online IPs
267         line = next(output).strip()
268         state.online_ips = regexp('list of online ips\s*:\s*(.*)', line)\
269             .groups()[0].split()
270         if len(state.online_ips) == 1 \
271                 and state.online_ips[0].strip() == 'NONE':
272             state.online_ips = []
273         line = next(output).strip()
274         assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
275
276         # VPNs
277         state.connected_vpns = []
278         line = next(output).strip()
279         assert line == 'vpns connected:', \
280             'expected vpns connected, got {0}'.format(line)
281         for line in output:
282             line = line.strip()
283             if len(line) == 0:
284                 continue
285             elif line == 'end of list of connected vpns':
286                 break
287             else:
288                 state.connected_vpns.append(line)
289         # end: for lines
290         line = next(output).strip()
291         assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
292
293         # log level
294         line = next(output).strip()
295         state.log_level, state.log_file = \
296             regexp('Logging with level (.+)(?:\s+to\s+(.+))?', line).groups()
297
298         # done
299         line = next(output).strip()
300         assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
301         line = next(output).strip()
302         assert line == 'Done.', 'expect Done but got {0}'.format(line)
303
304         return state
305     # end: ConndState.get_state
306
307 # end: class ConndState
308
309
310 def test():
311     """ get state and print it """
312     state = ConndState.get_state()
313     print(state)
314     print(state.complete_str())
315
316
317 def main():
318     """ Main function, called when running file as script; runs test() """
319     test()
320 # end: function main
321
322
323 if __name__ == '__main__':
324     main()