Clean up, remove compat with py < 3.6
[pyi2ncommon] / src / dial.py
1 # This Python file uses the following encoding: utf-8
2
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
23 """
24
25 SUMMARY
26 ------------------------------------------------------
27 Dialing, hangup, general provider online/offline state control.
28
29 Copyright: 2017 Intra2net AG
30
31 This used to be part of the sysmisc utilitiy initially which caused an include
32 circle with the arnied wrapper.
33
34
35 CONTENTS
36 ------------------------------------------------------
37 dialout
38     Generic access to the system’s dialing mode. Allows for requesting manual
39     or permanently online dial state.
40
41 arnied_dial_permanent
42     Enter permantly online dialing state.
43
44 arnied_dial_do
45     Enter dial on command state.
46
47 arnied_dial_hangup
48     Terminate uplink unconditionally.
49
50 All API methods come with the optional argument ``block`` (bool) to request the
51 call to block until the system completes the state transition successfully. The
52 completion timeout is currently 10 seconds (see the definition of
53 ``DIALTOOLS_TIMEOUT`` below).
54
55
56 IMPLEMENTATION
57 ------------------------------------------------------
58
59 """
60
61
62 import re
63 import io
64 import time
65 import logging
66 log = logging.getLogger('pyi2ncommon.dial')
67
68 from . import arnied_wrapper
69 from . import simple_cnf
70 from . import sysmisc
71
72 HAVE_IPADDRESS = True
73 try:
74     import ipaddress
75 except ImportError:  # guest
76     HAVE_IPADDRESS = False
77
78 __all__ = (
79     "arnied_dial_hangup", "arnied_dial_doc", "arnied_dial_permanent", "dialout", "get_wan_address", "DIALOUT_MODE_PERMANENT", "DIALOUT_MODE_MANUAL", "DIALOUT_MODE_DEFAULT", "DIALOUT_MODE_BY_NAME"
80 )
81
82 DIALOUT_MODE_PERMANENT = 0
83 DIALOUT_MODE_MANUAL = 1
84 DIALOUT_MODE_DEFAULT = DIALOUT_MODE_PERMANENT
85 DIALOUT_MODE_BY_NAME = {"permanent": DIALOUT_MODE_PERMANENT, "manual": DIALOUT_MODE_MANUAL}
86 DIALOUT_MODE_CNF = {DIALOUT_MODE_PERMANENT: "ONLINE", DIALOUT_MODE_MANUAL: "MANUAL"}
87
88 # compiling this regex needs the provider id and is postponed due to
89 # the peculiar implementation of the connd online condition
90 NEEDLE_MEMO = "  \[%s\] :(.*connected online fsm<online>.*)"
91 NEEDLE_OFFLINE = re.compile("connection map:\nend of connection map")
92
93 DIALTOOLS_HANGUP_BIN = "/usr/intranator/bin/hangup"
94
95 #: binary for manual dialing (dial on command)
96 DIALTOOLS_DOC_BIN = "/usr/intranator/bin/doc"
97 DIALTOOLS_TIMEOUT = 10  # s
98
99 #: client binary for talking to connd
100 TELL_CONND_BIN = "/usr/intranator/bin/tell-connd"
101
102
103 def _connd_online(prid="P1"):
104     succ, out, _ = sysmisc.run_cmd_with_pipe([TELL_CONND_BIN, "--status"])
105     if succ is False:
106         return False
107     return re.search(NEEDLE_MEMO % prid, out) is not None
108
109
110 def _connd_offline():
111     succ, out, _ = sysmisc.run_cmd_with_pipe([TELL_CONND_BIN, "--status"])
112     return succ and (NEEDLE_OFFLINE.search(out) is not None)
113
114
115 def arnied_dial_hangup(block=False):
116     """
117     Take down any currently active provider. This leverages arnied to
118     accomplish the disconnect which is apparently more appropriate than
119     having connd do it directly.
120
121     :returns:       Whether the ``hangup`` command succeeded.
122     :rtype:         int (dial result as above)
123     """
124     log.debug("requested arnied_dial_hangup%s",
125               " (blocking)" if block else "")
126     if block is False:
127         succ, _, _ = sysmisc.run_cmd_with_pipe([DIALTOOLS_HANGUP_BIN])
128         return sysmisc.RUN_RESULT_OK if succ is True else sysmisc.RUN_RESULT_FAIL
129
130     res, err = sysmisc.cmd_block_till([DIALTOOLS_HANGUP_BIN],
131                                       DIALTOOLS_TIMEOUT, _connd_offline)
132     log.debug("arnied_dial_hangup → (%d, %r)", res, err)
133     return res
134
135
136 def arnied_dial_doc(prid="P1", block=False):
137     """
138     Bring provider up via arnied manual dial (dial on command).
139
140     :param  prid:   Provider id, default *P1*. It is up to the caller to ensure
141                     this is a valid provider id.
142     :type   prid:   str
143     :param block:   block execution until system is online
144     :type  block:   bool
145     :returns:       Whether the ``doc`` command succeeded.
146     :rtype:         int (dial result as above)
147     """
148     log.debug("requested arnied_dial_doc%s", " (blocking)" if block else "")
149     if block is False:
150         succ, _, _ = sysmisc.run_cmd_with_pipe([DIALTOOLS_DOC_BIN, prid])
151         return sysmisc.RUN_RESULT_OK if succ is True else sysmisc.RUN_RESULT_FAIL
152     res, err = sysmisc.cmd_block_till([DIALTOOLS_DOC_BIN, prid],
153                                       DIALTOOLS_TIMEOUT, _connd_online,
154                                       prid=prid)
155     log.debug("arnied_dial_doc → (%d, %r)", res, err)
156     return res
157
158
159 def arnied_dial_permanent(prid="P1", block=False):
160     """
161     Set permanent online state. Since the arnied dial helpers cannot initiate a
162     permanent online state, achieve this via arnied.
163
164     :param  prid:   Provider id, default *P1*. It is up to the caller to ensure
165                     this is a valid provider id.
166     :type   prid:   str
167     :param block:   block execution until system is online
168     :type  block:   bool
169     :returns:       Whether the ``tell-connd`` command succeeded.
170     :rtype:         int (dial result as above)
171     """
172     log.debug("requested connd_dial_online%s" % " (blocking)" if block else "")
173
174     cnf = simple_cnf.SimpleCnf()
175     cnf.add("DIALOUT_MODE", DIALOUT_MODE_CNF[DIALOUT_MODE_PERMANENT])
176     cnf.add("DIALOUT_DEFAULTPROVIDER_REF", "1")
177
178     def aux():
179         return arnied_wrapper.set_cnf_pipe(cnf, block=block), "", None
180
181     if block is False:
182         succ = aux()
183         return sysmisc.RUN_RESULT_OK if succ is True else sysmisc.RUN_RESULT_FAIL
184
185     res, err = sysmisc.cmd_block_till(aux, DIALTOOLS_TIMEOUT, _connd_online,
186                                       prid=prid)
187     log.debug("arnied_dial_permanent: result (%d, %r)", res, err)
188     return res
189
190
191 def dialout(mode=DIALOUT_MODE_DEFAULT, prid="P1", block=True):
192     """
193
194     :param  mode:   How to dial (permanent vs. manual).
195     :type   mode:   int (``DIALOUT_MODE``) | string
196     :param  prid:   Provider id, default *P1*. It is up to the caller to ensure
197                     this is a valid provider id.
198     :type   prid:   str (constrained by available providers, obviously).
199     :param block:   Whether to block until completion of the command.
200     :type  block:   bool
201     :returns:       Whether the command succeeded.
202     :rtype:         int (dial result)
203     :raises:        :py:class:`ValueError` if invalid dial mode was selected
204     """
205     log.info("go online with provider")
206
207     dmode = None
208     if isinstance(mode, int) is True:
209         dmode = mode
210     elif isinstance(mode, str) is True:
211         try:
212             dmode = DIALOUT_MODE_BY_NAME[mode]
213         except Exception:
214             log.error("invalid online mode name “%s” requested" % mode)
215             pass
216
217     if dmode is None:
218         raise ValueError("exiting due to invalid online mode %r" % mode)
219
220     log.debug("go online, mode=%d(%s), id=%r", dmode, mode, prid)
221
222     if dmode == DIALOUT_MODE_PERMANENT:
223         return arnied_dial_permanent(prid, block=block)
224
225     if dmode == DIALOUT_MODE_MANUAL:
226         return arnied_dial_doc(prid, block=block)
227
228     raise ValueError("invalid dialout mode %r/%r requested" % (mode, dmode))
229
230
231 def get_wan_address(vm=None):
232     """
233     Retrieve the current WAN IP address of client ``vm`` or localhost.
234
235     :param     vm:  Guest (client) to query; will as local connd if left unspecified.
236     :type      vm:  virttest.qemu_vm.VM | None
237
238     :returns:       The IPv4 address. For correctness, it will use the
239                     ipaddress module if available. Otherwise it falls back
240                     on untyped data.
241     :rtype:         None | (ipaddress.IPv4Address | str)
242     """
243     log.info("query current lease")
244     if vm is None:
245         succ, connstat, _ = sysmisc.run_cmd_with_pipe([TELL_CONND_BIN, "--status"])
246         if succ is False:
247             return None
248     else:
249         connstat = vm.session.cmd_output("%s --status" % TELL_CONND_BIN)
250     astr = io.StringIO(connstat)
251
252     while True:
253         l = astr.readline()
254         if l == "":
255             break
256         if l.find("connected online fsm<online> IP:") != -1:
257             addr = l[l.find("IP:")+3:l.find(" )\n")]  # beurk
258             if HAVE_IPADDRESS is True:
259                 return ipaddress.IPv4Address(str(addr))
260             else:
261                 return addr
262     return None