Commit | Line | Data |
---|---|---|
f49f6323 PD |
1 | #!/usr/bin/env python |
2 | ||
11cbb815 PD |
3 | # The software in this package is distributed under the GNU General |
4 | # Public License version 2 (with a special exception described below). | |
5 | # | |
6 | # A copy of GNU General Public License (GPL) is included in this distribution, | |
7 | # in the file COPYING.GPL. | |
8 | # | |
9 | # As a special exception, if other files instantiate templates or use macros | |
10 | # or inline functions from this file, or you compile this file and link it | |
11 | # with other works to produce a work based on this file, this file | |
12 | # does not by itself cause the resulting work to be covered | |
13 | # by the GNU General Public License. | |
14 | # | |
15 | # However the source code for this file must still be made available | |
16 | # in accordance with section (3) of the GNU General Public License. | |
17 | # | |
18 | # This exception does not invalidate any other reasons why a work based | |
19 | # on this file might be covered by the GNU General Public License. | |
20 | # | |
21 | # Copyright (c) 2016-2018 Intra2net AG <info@intra2net.com> | |
22 | ||
f49f6323 PD |
23 | """ |
24 | ||
25 | SUMMARY | |
26 | ------------------------------------------------------ | |
27 | Representation for connd state as returned by "tell-connd --status". | |
28 | ||
29 | Copyright: Intra2net AG | |
30 | ||
31 | ||
32 | INTERFACE | |
33 | ------------------------------------------------------ | |
34 | ||
35 | """ | |
36 | ||
f49f6323 PD |
37 | import subprocess |
38 | from re import match as regexp | |
39 | from os import EX_OK | |
40 | ||
41 | # constants | |
42 | DEFAULT_TELL_CONND_BINARY = '/usr/intranator/bin/tell-connd' | |
7628bc48 | 43 | TIMEOUT = 10 |
f49f6323 PD |
44 | |
45 | ONLINE_MODE_ALWAYS_ONLINE = 'always online' | |
46 | ONLINE_MODE_ALWAYS_OFFLINE = 'always offline' | |
47 | ONLINE_MODE_DIAL_ON_COMMAND = 'dial on command' | |
48 | ONLINE_MODE_DIAL_ON_DEMAND = 'dial on demand' | |
49 | ||
50 | SUBSYS_DNS = 'dns' | |
51 | SUBSYS_DYNDNS = 'dyndns' | |
52 | SUBSYS_MAIL = 'mail' | |
53 | SUBSYS_NTP = 'ntp' | |
54 | SUBSYS_SOCKS = 'socks' | |
55 | SUBSYS_VPN = 'vpn' | |
56 | SUBSYS_WEBPROXY = 'webproxy' | |
57 | SUBSYS_PINGCHECK = 'pingcheck' | |
58 | SUBSYS_IPONLINE = 'iponline' | |
59 | ALL_SUBSYS = (SUBSYS_DNS, SUBSYS_DYNDNS, SUBSYS_MAIL, SUBSYS_NTP, | |
60 | SUBSYS_SOCKS, SUBSYS_VPN, SUBSYS_WEBPROXY, SUBSYS_PINGCHECK, | |
61 | SUBSYS_IPONLINE) | |
62 | ||
63 | ALL_MODES = (ONLINE_MODE_DIAL_ON_DEMAND, ONLINE_MODE_DIAL_ON_COMMAND, | |
64 | ONLINE_MODE_ALWAYS_OFFLINE, ONLINE_MODE_ALWAYS_ONLINE) | |
65 | ||
66 | ||
67 | class ConndState(object): | |
68 | """Representation of connd's status as returned by tell-connd --status.""" | |
69 | ||
70 | online_mode = None | |
71 | default_provider = None | |
72 | subsys_online = None | |
73 | subsys_offline = None | |
74 | subsys_disabled = None | |
75 | connections = None | |
76 | actions = None | |
77 | online_ips = None | |
78 | connected_vpns = None | |
79 | log_level = None | |
80 | log_file = None | |
81 | ||
82 | def __str__(self): | |
83 | return \ | |
7628bc48 | 84 | '[ConndState: {0} (default {1}), {2} conn\'s, {3} ips, {4} vpns ]' \ |
f49f6323 PD |
85 | .format(self.online_mode, self.default_provider, |
86 | len(self.connections), len(self.online_ips), | |
87 | len(self.connected_vpns)) | |
88 | ||
89 | def complete_str(self): | |
90 | """Return a string representating the complete state.""" | |
91 | ||
92 | # general | |
93 | parts = [ | |
94 | 'ConndState: online mode = "{0}" (default provider: {1})\n' | |
95 | .format(self.online_mode, self.default_provider), ] | |
96 | ||
97 | # subsys | |
98 | # ' connctns: (repeated here for correct aligning) | |
99 | parts.append(' subsys: online: ') | |
100 | if self.subsys_online: | |
101 | for subsys in self.subsys_online: | |
102 | parts.append(subsys + ' ') | |
103 | else: | |
104 | parts.append('None ') | |
105 | parts.append('; offline: ') | |
106 | if self.subsys_offline: | |
107 | for subsys in self.subsys_offline: | |
108 | parts.append(subsys + ' ') | |
109 | else: | |
110 | parts.append('None ') | |
111 | parts.append('; disabled: ') | |
112 | if self.subsys_disabled: | |
113 | for subsys in self.subsys_disabled: | |
114 | parts.append(subsys + ' ') | |
115 | else: | |
116 | parts.append('None') | |
117 | parts.append('\n') | |
118 | ||
119 | # connections | |
120 | parts.append(' conns: ') | |
121 | if self.connections: | |
122 | name, info, actions = self.connections[0] | |
123 | parts.append('{0}: {1}, {2}\n'.format(name, info, actions)) | |
124 | else: | |
125 | parts.append('None\n') | |
126 | for name, info, actions in self.connections[1:]: | |
127 | # ' connctns: (repeated here for correct aligning) | |
128 | parts.append(' {0}: {1}, {2}\n'.format(name, info, | |
129 | actions)) | |
130 | ||
131 | # actions | |
132 | # ' connctns: (repeated here for correct aligning) | |
133 | parts.append(' actions: ') | |
134 | if self.actions: | |
135 | parts.append(self.actions[0] + '\n') | |
136 | else: | |
137 | parts.append('None\n') | |
138 | for action in self.actions[1:]: | |
139 | # ' connctns: (repeated here for correct aligning) | |
140 | parts.append(' {0}\n'.format(action)) | |
141 | ||
142 | # online IPs | |
143 | # ' connctns: (repeated here for correct aligning) | |
144 | parts.append(' IPs: ') | |
145 | if self.online_ips: | |
146 | parts.append(self.online_ips[0]) | |
147 | for curr_ip in self.online_ips[1:]: | |
148 | parts.append(', {0}'.format(curr_ip)) | |
149 | else: | |
150 | parts.append('None') | |
151 | parts.append('\n') | |
152 | ||
153 | # VPNs | |
154 | # ' connctns: (repeated here for correct aligning) | |
155 | parts.append(' VPNs: ') | |
156 | if self.connected_vpns: | |
157 | parts.append(self.connected_vpns[0]) | |
158 | for vpn in self.connected_vpns[1:]: | |
159 | parts.append(', {0}'.format(vpn)) | |
160 | else: | |
161 | parts.append('None') | |
162 | parts.append('\n') | |
163 | ||
164 | # log level and target: | |
165 | # ' connctns: (repeated here for correct aligning) | |
166 | parts.append(' Log: level {0}'.format(self.log_level)) | |
167 | if self.log_file: | |
168 | parts.append(' to {0}'.format(self.log_file)) | |
169 | parts.append('\n') | |
170 | ||
171 | return ''.join(parts) | |
172 | ||
173 | @staticmethod | |
174 | def run_tell_connd(tell_connd_binary=DEFAULT_TELL_CONND_BINARY, *args): | |
175 | """ | |
176 | Run "tell-connd --status", return output iterator and return code. | |
177 | ||
178 | Catches all it can, so should usually return (output, return_code) | |
179 | where output = [line1, line2, ...] | |
180 | ||
181 | If return_code != 0, output's first line(s) is error message. | |
182 | ||
183 | .. todo:: Use reST parameter description here. | |
184 | """ | |
185 | try: | |
186 | cmd_parts = [tell_connd_binary, ] | |
187 | cmd_parts.extend(*args) | |
188 | output = subprocess.check_output(cmd_parts, | |
189 | stderr=subprocess.STDOUT, | |
7628bc48 CH |
190 | universal_newlines=True, shell=False, |
191 | timeout=TIMEOUT) | |
f49f6323 PD |
192 | return EX_OK, output.splitlines() |
193 | except subprocess.CalledProcessError as cpe: # non-zero return status | |
194 | output = [ | |
195 | 'tell-connd exited with status {0}'.format(cpe.returncode), ] | |
196 | output.extend(cpe.output.splitlines()) | |
197 | return cpe.returncode, output | |
7628bc48 CH |
198 | except subprocess.TimeoutExpired as texp: |
199 | output = [f'tell-connd timed out after {texp.timeout}s. Returning -1', ] | |
200 | output.extend(texp.output.splitlines()) | |
201 | return -1, output | |
f49f6323 PD |
202 | except Exception as exp: |
203 | output = [str(exp), ] | |
204 | return -1, output | |
205 | ||
206 | @staticmethod | |
207 | def get_state(tell_connd_binary=DEFAULT_TELL_CONND_BINARY): | |
208 | """ | |
209 | Get actual state from "tell-connd --status". | |
210 | ||
211 | Returns (err_code, output_lines) if something goes wrong running | |
212 | binary; raises assertion if output from tell-connd does not match | |
213 | expected format. | |
214 | ||
215 | .. todo:: Use reST parameter description here. | |
216 | """ | |
217 | ||
218 | state = ConndState() | |
219 | ||
220 | err_code, all_lines = ConndState.run_tell_connd(tell_connd_binary, | |
221 | ['--status', ]) | |
222 | if err_code != EX_OK: | |
223 | return err_code, all_lines | |
224 | ||
225 | output = iter(all_lines) | |
226 | ||
227 | # first section | |
228 | line = next(output).strip() | |
229 | state.online_mode = regexp('online mode\s*:\s*(.+)$', line).groups()[0] | |
230 | assert state.online_mode in ALL_MODES, \ | |
231 | 'unexpected online mode: {0}'.format(state.online_mode) | |
232 | ||
233 | line = next(output).strip() | |
234 | state.default_provider = regexp('default provider\s*:\s*(.*)$', | |
235 | line).groups()[0] | |
236 | if len(state.default_provider) == 0: | |
237 | state.default_provider = None | |
238 | line = next(output).strip() | |
239 | assert len(line) == 0, 'expected empty line, but got {0}'.format(line) | |
240 | ||
241 | # subsys | |
242 | line = next(output).strip() | |
243 | assert line == 'subsys', 'expected subsys but got {0}'.format(line) | |
244 | line = next(output).strip() | |
7628bc48 | 245 | state.subsys_online = regexp('online\s*:\s*(.*)$', line) \ |
f49f6323 PD |
246 | .groups()[0].split() |
247 | for subsys in state.subsys_online: | |
248 | assert subsys in ALL_SUBSYS, \ | |
249 | 'unexpected subsys: {0}'.format(subsys) | |
250 | line = next(output).strip() | |
7628bc48 | 251 | state.subsys_offline = regexp('offline\s*:\s*(.*)$', line) \ |
f49f6323 PD |
252 | .groups()[0].split() |
253 | for subsys in state.subsys_offline: | |
254 | assert subsys in ALL_SUBSYS, \ | |
255 | 'unexpected subsys: {0}'.format(subsys) | |
256 | line = next(output).strip() | |
7628bc48 | 257 | state.subsys_disabled = regexp('disabled\s*:\s*(.*)$', line) \ |
f49f6323 PD |
258 | .groups()[0].split() |
259 | for subsys in state.subsys_disabled: | |
260 | assert subsys in ALL_SUBSYS, \ | |
261 | 'unexpected subsys: {0}'.format(subsys) | |
262 | line = next(output).strip() | |
263 | assert len(line) == 0, 'expected empty line, but got {0}'.format(line) | |
264 | ||
265 | # connection map | |
266 | state.connections = [] | |
267 | line = next(output).strip() | |
268 | assert line == 'connection map:', \ | |
269 | 'expected connection map but got {0}'.format(line) | |
270 | expect_new = True | |
7628bc48 CH |
271 | conn_name = None |
272 | conn_info = None | |
f49f6323 PD |
273 | for line in output: |
274 | line = line.strip() | |
275 | if len(line) == 0: | |
276 | continue | |
277 | if expect_new: | |
278 | if line == 'end of connection map': | |
279 | break | |
280 | conn_name, conn_info = regexp( | |
281 | '\[\s*(.+)\s*\]\s*:\s*\(\s*(.*)\s*\)', line).groups() | |
282 | expect_new = False | |
283 | else: | |
7628bc48 | 284 | conn_actions = regexp('actions\s*:\s*\[\s*(.+)\s*\]', line) \ |
f49f6323 | 285 | .groups() |
7628bc48 CH |
286 | assert conn_name is not None and conn_info is not None, \ |
287 | 'error parsing connection maps' | |
f49f6323 PD |
288 | state.connections.append((conn_name, conn_info, conn_actions)) |
289 | expect_new = True | |
7628bc48 CH |
290 | conn_name = None |
291 | conn_info = None | |
f49f6323 PD |
292 | assert expect_new |
293 | line = next(output).strip() | |
294 | assert len(line) == 0, 'expected empty line, but got {0}'.format(line) | |
295 | ||
296 | # actions | |
297 | line = next(output).strip() | |
298 | state.actions = regexp('actions\s*:\s*(.*)', line).groups()[0].split() | |
299 | if len(state.actions) == 1 and state.actions[0].strip() == '-': | |
300 | state.actions = [] | |
301 | line = next(output).strip() | |
302 | assert len(line) == 0, 'expected empty line, but got {0}'.format(line) | |
303 | ||
304 | # online IPs | |
305 | line = next(output).strip() | |
7628bc48 | 306 | state.online_ips = regexp('list of online ips\s*:\s*(.*)', line) \ |
f49f6323 PD |
307 | .groups()[0].split() |
308 | if len(state.online_ips) == 1 \ | |
309 | and state.online_ips[0].strip() == 'NONE': | |
310 | state.online_ips = [] | |
311 | line = next(output).strip() | |
312 | assert len(line) == 0, 'expected empty line, but got {0}'.format(line) | |
313 | ||
314 | # VPNs | |
315 | state.connected_vpns = [] | |
316 | line = next(output).strip() | |
317 | assert line == 'vpns connected:', \ | |
318 | 'expected vpns connected, got {0}'.format(line) | |
319 | for line in output: | |
320 | line = line.strip() | |
321 | if len(line) == 0: | |
322 | continue | |
323 | elif line == 'end of list of connected vpns': | |
324 | break | |
325 | else: | |
326 | state.connected_vpns.append(line) | |
327 | line = next(output).strip() | |
328 | assert len(line) == 0, 'expected empty line, but got {0}'.format(line) | |
329 | ||
330 | # log level | |
331 | line = next(output).strip() | |
332 | state.log_level, state.log_file = \ | |
333 | regexp('Logging with level (.+)(?:\s+to\s+(.+))?', line).groups() | |
334 | ||
335 | # done | |
336 | line = next(output).strip() | |
337 | assert len(line) == 0, 'expected empty line, but got {0}'.format(line) | |
338 | line = next(output).strip() | |
339 | assert line == 'Done.', 'expect Done but got {0}'.format(line) | |
340 | ||
341 | return state | |
342 | ||
343 | @staticmethod | |
344 | def set_online_mode(state, provider=None, | |
345 | tell_connd_binary=DEFAULT_TELL_CONND_BINARY): | |
346 | """ | |
347 | Change online state with optional provider. | |
348 | ||
349 | Provider is silently ignored for ONLINE_MODE_ALWAYS_OFFLINE and | |
350 | otherwise required. | |
351 | ||
352 | Returns result of :py:func:`run_tell_connd`: (error_code, output_lines). | |
353 | """ | |
354 | ||
355 | # check args | |
356 | need_provider = True | |
357 | if state == ONLINE_MODE_DIAL_ON_DEMAND: | |
358 | args = ['--dial-on-demand', provider] | |
359 | elif state == ONLINE_MODE_DIAL_ON_COMMAND: | |
360 | args = ['--dial-on-command', provider] | |
361 | elif state == ONLINE_MODE_ALWAYS_ONLINE: | |
362 | args = ['--online', provider] | |
363 | elif state == ONLINE_MODE_ALWAYS_OFFLINE: | |
364 | args = ['--offline', ] | |
365 | need_provider = False | |
366 | else: | |
367 | raise ValueError('unknown state: {0}!'.format(state)) | |
368 | if need_provider and not provider: | |
369 | raise ValueError('Given state {0} requires a provider!'.format( | |
370 | state)) | |
371 | ||
372 | # run binary | |
373 | return ConndState.run_tell_connd(tell_connd_binary, args) | |
374 | ||
375 | ||
376 | def test(): | |
377 | """Get state and print it.""" | |
378 | state = ConndState.get_state() | |
379 | if not isinstance(state, ConndState): | |
380 | err_code, output_lines = state | |
381 | print('tell-connd failed with error code {0} and output:'.format( | |
382 | err_code)) | |
383 | for line in output_lines: | |
384 | print('tell-connd: {0}'.format(line)) | |
385 | print('(end of tell-connd output)') | |
386 | else: | |
387 | print(state) | |
388 | print(state.complete_str()) | |
389 | ||
390 | ||
391 | def main(): | |
392 | """Main function, called when running file as script; runs test().""" | |
393 | test() | |
394 | ||
395 | ||
396 | if __name__ == '__main__': | |
397 | main() |