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