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