Clean up, remove compat with py < 3.6
[pyi2ncommon] / src / connd_state.py
CommitLineData
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
25SUMMARY
26------------------------------------------------------
27Representation for connd state as returned by "tell-connd --status".
28
29Copyright: Intra2net AG
30
31
32INTERFACE
33------------------------------------------------------
34
35"""
36
f49f6323
PD
37import subprocess
38from re import match as regexp
39from os import EX_OK
40
41# constants
42DEFAULT_TELL_CONND_BINARY = '/usr/intranator/bin/tell-connd'
7628bc48 43TIMEOUT = 10
f49f6323
PD
44
45ONLINE_MODE_ALWAYS_ONLINE = 'always online'
46ONLINE_MODE_ALWAYS_OFFLINE = 'always offline'
47ONLINE_MODE_DIAL_ON_COMMAND = 'dial on command'
48ONLINE_MODE_DIAL_ON_DEMAND = 'dial on demand'
49
50SUBSYS_DNS = 'dns'
51SUBSYS_DYNDNS = 'dyndns'
52SUBSYS_MAIL = 'mail'
53SUBSYS_NTP = 'ntp'
54SUBSYS_SOCKS = 'socks'
55SUBSYS_VPN = 'vpn'
56SUBSYS_WEBPROXY = 'webproxy'
57SUBSYS_PINGCHECK = 'pingcheck'
58SUBSYS_IPONLINE = 'iponline'
59ALL_SUBSYS = (SUBSYS_DNS, SUBSYS_DYNDNS, SUBSYS_MAIL, SUBSYS_NTP,
60 SUBSYS_SOCKS, SUBSYS_VPN, SUBSYS_WEBPROXY, SUBSYS_PINGCHECK,
61 SUBSYS_IPONLINE)
62
63ALL_MODES = (ONLINE_MODE_DIAL_ON_DEMAND, ONLINE_MODE_DIAL_ON_COMMAND,
64 ONLINE_MODE_ALWAYS_OFFLINE, ONLINE_MODE_ALWAYS_ONLINE)
65
66
67class 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
376def 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
391def main():
392 """Main function, called when running file as script; runs test()."""
393 test()
394
395
396if __name__ == '__main__':
397 main()