Drop localized dialout cnfvar complexity for the easier cnf store
[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     log.debug("requested connd_dial_online%s" % " (blocking)" if block else "")
172
173     cnf = cnfvar.CnfList([("dialout_mode", DIALOUT_MODE_CNF[DIALOUT_MODE_PERMANENT]),
174                           ("dialout_defaultprovider_ref", "1")])
175
176     def aux():
177         store = cnfvar.BinaryCnfStore()
178         store.commit(cnf)
179         return True, "", None
180
181     if block is False:
182         succ, out, _ = 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