3 # The software in this package is distributed under the GNU General
4 # Public License version 2 (with a special exception described below).
6 # A copy of GNU General Public License (GPL) is included in this distribution,
7 # in the file COPYING.GPL.
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.
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.
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.
21 # Copyright (c) 2016-2018 Intra2net AG <info@intra2net.com>
26 ------------------------------------------------------
27 Representation for connd state as returned by "tell-connd --status".
29 Copyright: Intra2net AG
33 ------------------------------------------------------
38 from re import match as regexp
42 DEFAULT_TELL_CONND_BINARY = '/usr/intranator/bin/tell-connd'
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'
51 SUBSYS_DYNDNS = 'dyndns'
54 SUBSYS_SOCKS = 'socks'
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,
63 ALL_MODES = (ONLINE_MODE_DIAL_ON_DEMAND, ONLINE_MODE_DIAL_ON_COMMAND,
64 ONLINE_MODE_ALWAYS_OFFLINE, ONLINE_MODE_ALWAYS_ONLINE)
67 class ConndState(object):
68 """Representation of connd's status as returned by tell-connd --status."""
71 default_provider = None
74 subsys_disabled = None
84 '[ConndState: {0} (default {1}), {2} conn\'s, {3} ips, {4} vpns ]' \
85 .format(self.online_mode, self.default_provider,
86 len(self.connections), len(self.online_ips),
87 len(self.connected_vpns))
89 def complete_str(self):
90 """Return a string representating the complete state."""
94 'ConndState: online mode = "{0}" (default provider: {1})\n'
95 .format(self.online_mode, self.default_provider), ]
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 + ' ')
104 parts.append('None ')
105 parts.append('; offline: ')
106 if self.subsys_offline:
107 for subsys in self.subsys_offline:
108 parts.append(subsys + ' ')
110 parts.append('None ')
111 parts.append('; disabled: ')
112 if self.subsys_disabled:
113 for subsys in self.subsys_disabled:
114 parts.append(subsys + ' ')
120 parts.append(' conns: ')
122 name, info, actions = self.connections[0]
123 parts.append('{0}: {1}, {2}\n'.format(name, info, actions))
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,
132 # ' connctns: (repeated here for correct aligning)
133 parts.append(' actions: ')
135 parts.append(self.actions[0] + '\n')
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))
143 # ' connctns: (repeated here for correct aligning)
144 parts.append(' IPs: ')
146 parts.append(self.online_ips[0])
147 for curr_ip in self.online_ips[1:]:
148 parts.append(', {0}'.format(curr_ip))
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))
164 # log level and target:
165 # ' connctns: (repeated here for correct aligning)
166 parts.append(' Log: level {0}'.format(self.log_level))
168 parts.append(' to {0}'.format(self.log_file))
171 return ''.join(parts)
174 def run_tell_connd(tell_connd_binary=DEFAULT_TELL_CONND_BINARY, *args):
176 Run "tell-connd --status", return output iterator and return code.
178 Catches all it can, so should usually return (output, return_code)
179 where output = [line1, line2, ...]
181 If return_code != 0, output's first line(s) is error message.
183 .. todo:: Use reST parameter description here.
186 cmd_parts = [tell_connd_binary, ]
187 cmd_parts.extend(*args)
188 output = subprocess.check_output(cmd_parts,
189 stderr=subprocess.STDOUT,
190 universal_newlines=True, shell=False,
192 return EX_OK, output.splitlines()
193 except subprocess.CalledProcessError as cpe: # non-zero return status
195 'tell-connd exited with status {0}'.format(cpe.returncode), ]
196 output.extend(cpe.output.splitlines())
197 return cpe.returncode, output
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())
202 except Exception as exp:
203 output = [str(exp), ]
207 def get_state(tell_connd_binary=DEFAULT_TELL_CONND_BINARY):
209 Get actual state from "tell-connd --status".
211 Returns (err_code, output_lines) if something goes wrong running
212 binary; raises assertion if output from tell-connd does not match
215 .. todo:: Use reST parameter description here.
220 err_code, all_lines = ConndState.run_tell_connd(tell_connd_binary,
222 if err_code != EX_OK:
223 return err_code, all_lines
225 output = iter(all_lines)
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)
233 line = next(output).strip()
234 state.default_provider = regexp('default provider\s*:\s*(.*)$',
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)
242 line = next(output).strip()
243 assert line == 'subsys', 'expected subsys but got {0}'.format(line)
244 line = next(output).strip()
245 state.subsys_online = regexp('online\s*:\s*(.*)$', line) \
247 for subsys in state.subsys_online:
248 assert subsys in ALL_SUBSYS, \
249 'unexpected subsys: {0}'.format(subsys)
250 line = next(output).strip()
251 state.subsys_offline = regexp('offline\s*:\s*(.*)$', line) \
253 for subsys in state.subsys_offline:
254 assert subsys in ALL_SUBSYS, \
255 'unexpected subsys: {0}'.format(subsys)
256 line = next(output).strip()
257 state.subsys_disabled = regexp('disabled\s*:\s*(.*)$', line) \
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)
266 state.connections = []
267 line = next(output).strip()
268 assert line == 'connection map:', \
269 'expected connection map but got {0}'.format(line)
278 if line == 'end of connection map':
280 conn_name, conn_info = regexp(
281 '\[\s*(.+)\s*\]\s*:\s*\(\s*(.*)\s*\)', line).groups()
284 conn_actions = regexp('actions\s*:\s*\[\s*(.+)\s*\]', line) \
286 assert conn_name is not None and conn_info is not None, \
287 'error parsing connection maps'
288 state.connections.append((conn_name, conn_info, conn_actions))
293 line = next(output).strip()
294 assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
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() == '-':
301 line = next(output).strip()
302 assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
305 line = next(output).strip()
306 state.online_ips = regexp('list of online ips\s*:\s*(.*)', line) \
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)
315 state.connected_vpns = []
316 line = next(output).strip()
317 assert line == 'vpns connected:', \
318 'expected vpns connected, got {0}'.format(line)
323 elif line == 'end of list of connected vpns':
326 state.connected_vpns.append(line)
327 line = next(output).strip()
328 assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
331 line = next(output).strip()
332 state.log_level, state.log_file = \
333 regexp('Logging with level (.+)(?:\s+to\s+(.+))?', line).groups()
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)
344 def set_online_mode(state, provider=None,
345 tell_connd_binary=DEFAULT_TELL_CONND_BINARY):
347 Change online state with optional provider.
349 Provider is silently ignored for ONLINE_MODE_ALWAYS_OFFLINE and
352 Returns result of :py:func:`run_tell_connd`: (error_code, output_lines).
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
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(
373 return ConndState.run_tell_connd(tell_connd_binary, args)
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(
383 for line in output_lines:
384 print('tell-connd: {0}'.format(line))
385 print('(end of tell-connd output)')
388 print(state.complete_str())
392 """Main function, called when running file as script; runs test()."""
396 if __name__ == '__main__':