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>
24 Representation for connd state as returned by "tell-connd --status".
26 Copyright: Intra2net AG
30 from re import match as regexp
34 DEFAULT_TELL_CONND_BINARY = '/usr/intranator/bin/tell-connd'
37 ONLINE_MODE_ALWAYS_ONLINE = 'always online'
38 ONLINE_MODE_ALWAYS_OFFLINE = 'always offline'
39 ONLINE_MODE_DIAL_ON_COMMAND = 'dial on command'
40 ONLINE_MODE_DIAL_ON_DEMAND = 'dial on demand'
43 SUBSYS_DYNDNS = 'dyndns'
46 SUBSYS_SOCKS = 'socks'
48 SUBSYS_WEBPROXY = 'webproxy'
49 SUBSYS_PINGCHECK = 'pingcheck'
50 SUBSYS_IPONLINE = 'iponline'
51 ALL_SUBSYS = (SUBSYS_DNS, SUBSYS_DYNDNS, SUBSYS_MAIL, SUBSYS_NTP,
52 SUBSYS_SOCKS, SUBSYS_VPN, SUBSYS_WEBPROXY, SUBSYS_PINGCHECK,
55 ALL_MODES = (ONLINE_MODE_DIAL_ON_DEMAND, ONLINE_MODE_DIAL_ON_COMMAND,
56 ONLINE_MODE_ALWAYS_OFFLINE, ONLINE_MODE_ALWAYS_ONLINE)
59 class ConndState(object):
60 """Representation of connd's status as returned by tell-connd --status."""
63 default_provider = None
66 subsys_disabled = None
76 '[ConndState: {0} (default {1}), {2} conn\'s, {3} ips, {4} vpns ]' \
77 .format(self.online_mode, self.default_provider,
78 len(self.connections), len(self.online_ips),
79 len(self.connected_vpns))
81 def complete_str(self):
82 """Return a string representating the complete state."""
86 'ConndState: online mode = "{0}" (default provider: {1})\n'
87 .format(self.online_mode, self.default_provider), ]
90 # ' connctns: (repeated here for correct aligning)
91 parts.append(' subsys: online: ')
92 if self.subsys_online:
93 for subsys in self.subsys_online:
94 parts.append(subsys + ' ')
97 parts.append('; offline: ')
98 if self.subsys_offline:
99 for subsys in self.subsys_offline:
100 parts.append(subsys + ' ')
102 parts.append('None ')
103 parts.append('; disabled: ')
104 if self.subsys_disabled:
105 for subsys in self.subsys_disabled:
106 parts.append(subsys + ' ')
112 parts.append(' conns: ')
114 name, info, actions = self.connections[0]
115 parts.append('{0}: {1}, {2}\n'.format(name, info, actions))
117 parts.append('None\n')
118 for name, info, actions in self.connections[1:]:
119 # ' connctns: (repeated here for correct aligning)
120 parts.append(' {0}: {1}, {2}\n'.format(name, info,
124 # ' connctns: (repeated here for correct aligning)
125 parts.append(' actions: ')
127 parts.append(self.actions[0] + '\n')
129 parts.append('None\n')
130 for action in self.actions[1:]:
131 # ' connctns: (repeated here for correct aligning)
132 parts.append(' {0}\n'.format(action))
135 # ' connctns: (repeated here for correct aligning)
136 parts.append(' IPs: ')
138 parts.append(self.online_ips[0])
139 for curr_ip in self.online_ips[1:]:
140 parts.append(', {0}'.format(curr_ip))
146 # ' connctns: (repeated here for correct aligning)
147 parts.append(' VPNs: ')
148 if self.connected_vpns:
149 parts.append(self.connected_vpns[0])
150 for vpn in self.connected_vpns[1:]:
151 parts.append(', {0}'.format(vpn))
156 # log level and target:
157 # ' connctns: (repeated here for correct aligning)
158 parts.append(' Log: level {0}'.format(self.log_level))
160 parts.append(' to {0}'.format(self.log_file))
163 return ''.join(parts)
166 def run_tell_connd(tell_connd_binary=DEFAULT_TELL_CONND_BINARY, *args):
168 Run "tell-connd --status", return output iterator and return code.
170 Catches all it can, so should usually return (output, return_code)
171 where output = [line1, line2, ...]
173 If return_code != 0, output's first line(s) is error message.
175 .. todo:: Use reST parameter description here.
178 cmd_parts = [tell_connd_binary, ]
179 cmd_parts.extend(*args)
180 output = subprocess.check_output(cmd_parts,
181 stderr=subprocess.STDOUT,
182 universal_newlines=True, shell=False,
184 return EX_OK, output.splitlines()
185 except subprocess.CalledProcessError as cpe: # non-zero return status
187 'tell-connd exited with status {0}'.format(cpe.returncode), ]
188 output.extend(cpe.output.splitlines())
189 return cpe.returncode, output
190 except subprocess.TimeoutExpired as texp:
191 output = [f'tell-connd timed out after {texp.timeout}s. Returning -1', ]
192 output.extend(texp.output.splitlines())
194 except Exception as exp:
195 output = [str(exp), ]
199 def get_state(tell_connd_binary=DEFAULT_TELL_CONND_BINARY):
201 Get actual state from "tell-connd --status".
203 Returns (err_code, output_lines) if something goes wrong running
204 binary; raises assertion if output from tell-connd does not match
207 .. todo:: Use reST parameter description here.
212 err_code, all_lines = ConndState.run_tell_connd(tell_connd_binary,
214 if err_code != EX_OK:
215 return err_code, all_lines
217 output = iter(all_lines)
220 line = next(output).strip()
221 state.online_mode = regexp('online mode\s*:\s*(.+)$', line).groups()[0]
222 assert state.online_mode in ALL_MODES, \
223 'unexpected online mode: {0}'.format(state.online_mode)
225 line = next(output).strip()
226 state.default_provider = regexp('default provider\s*:\s*(.*)$',
228 if len(state.default_provider) == 0:
229 state.default_provider = None
230 line = next(output).strip()
231 assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
234 line = next(output).strip()
235 assert line == 'subsys', 'expected subsys but got {0}'.format(line)
236 line = next(output).strip()
237 state.subsys_online = regexp('online\s*:\s*(.*)$', line) \
239 for subsys in state.subsys_online:
240 assert subsys in ALL_SUBSYS, \
241 'unexpected subsys: {0}'.format(subsys)
242 line = next(output).strip()
243 state.subsys_offline = regexp('offline\s*:\s*(.*)$', line) \
245 for subsys in state.subsys_offline:
246 assert subsys in ALL_SUBSYS, \
247 'unexpected subsys: {0}'.format(subsys)
248 line = next(output).strip()
249 state.subsys_disabled = regexp('disabled\s*:\s*(.*)$', line) \
251 for subsys in state.subsys_disabled:
252 assert subsys in ALL_SUBSYS, \
253 'unexpected subsys: {0}'.format(subsys)
254 line = next(output).strip()
255 assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
258 state.connections = []
259 line = next(output).strip()
260 assert line == 'connection map:', \
261 'expected connection map but got {0}'.format(line)
270 if line == 'end of connection map':
272 conn_name, conn_info = regexp(
273 '\[\s*(.+)\s*\]\s*:\s*\(\s*(.*)\s*\)', line).groups()
276 conn_actions = regexp('actions\s*:\s*\[\s*(.+)\s*\]', line) \
278 assert conn_name is not None and conn_info is not None, \
279 'error parsing connection maps'
280 state.connections.append((conn_name, conn_info, conn_actions))
285 line = next(output).strip()
286 assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
289 line = next(output).strip()
290 state.actions = regexp('actions\s*:\s*(.*)', line).groups()[0].split()
291 if len(state.actions) == 1 and state.actions[0].strip() == '-':
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.online_ips = regexp('list of online ips\s*:\s*(.*)', line) \
300 if len(state.online_ips) == 1 \
301 and state.online_ips[0].strip() == 'NONE':
302 state.online_ips = []
303 line = next(output).strip()
304 assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
307 state.connected_vpns = []
308 line = next(output).strip()
309 assert line == 'vpns connected:', \
310 'expected vpns connected, got {0}'.format(line)
315 elif line == 'end of list of connected vpns':
318 state.connected_vpns.append(line)
319 line = next(output).strip()
320 assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
323 line = next(output).strip()
324 state.log_level, state.log_file = \
325 regexp('Logging with level (.+)(?:\s+to\s+(.+))?', line).groups()
328 line = next(output).strip()
329 assert len(line) == 0, 'expected empty line, but got {0}'.format(line)
330 line = next(output).strip()
331 assert line == 'Done.', 'expect Done but got {0}'.format(line)
336 def set_online_mode(state, provider=None,
337 tell_connd_binary=DEFAULT_TELL_CONND_BINARY):
339 Change online state with optional provider.
341 Provider is silently ignored for ONLINE_MODE_ALWAYS_OFFLINE and
344 Returns result of :py:func:`run_tell_connd`: (error_code, output_lines).
349 if state == ONLINE_MODE_DIAL_ON_DEMAND:
350 args = ['--dial-on-demand', provider]
351 elif state == ONLINE_MODE_DIAL_ON_COMMAND:
352 args = ['--dial-on-command', provider]
353 elif state == ONLINE_MODE_ALWAYS_ONLINE:
354 args = ['--online', provider]
355 elif state == ONLINE_MODE_ALWAYS_OFFLINE:
356 args = ['--offline', ]
357 need_provider = False
359 raise ValueError('unknown state: {0}!'.format(state))
360 if need_provider and not provider:
361 raise ValueError('Given state {0} requires a provider!'.format(
365 return ConndState.run_tell_connd(tell_connd_binary, args)
369 """Get state and print it."""
370 state = ConndState.get_state()
371 if not isinstance(state, ConndState):
372 err_code, output_lines = state
373 print('tell-connd failed with error code {0} and output:'.format(
375 for line in output_lines:
376 print('tell-connd: {0}'.format(line))
377 print('(end of tell-connd output)')
380 print(state.complete_str())
384 """Main function, called when running file as script; runs test()."""
388 if __name__ == '__main__':