30e930ea54de1bc72451b2c094af3cb1a73645de
[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     :returns:       Whether the ``doc`` command succeeded.
144     :rtype:         int (dial result as above)
145     """
146     log.debug("requested arnied_dial_doc%s", " (blocking)" if block else "")
147     if block is False:
148         succ, _, _ = sysmisc.run_cmd_with_pipe([DIALTOOLS_DOC_BIN, prid])
149         return sysmisc.RUN_RESULT_OK if succ is True else sysmisc.RUN_RESULT_FAIL
150     res, err = sysmisc.cmd_block_till([DIALTOOLS_DOC_BIN, prid],
151                                       DIALTOOLS_TIMEOUT, _connd_online,
152                                       prid=prid)
153     log.debug("arnied_dial_doc → (%d, %r)", res, err)
154     return res
155
156
157 def arnied_dial_permanent(prid="P1", block=False):
158     """
159     Set permanent online state. Since the arnied dial helpers cannot initiate a
160     permanent online state, achieve this via arnied.
161
162     :param  prid:   Provider id, default *P1*. It is up to the caller to ensure
163                     this is a valid provider id.
164     :type   prid:   str
165     :returns:       Whether the ``tell-connd`` command succeeded.
166     :rtype:         int (dial result as above)
167     """
168     log.debug("requested connd_dial_online%s" % " (blocking)" if block else "")
169
170     cnf = simple_cnf.SimpleCnf()
171     cnf.add("DIALOUT_MODE", DIALOUT_MODE_CNF[DIALOUT_MODE_PERMANENT])
172     cnf.add("DIALOUT_DEFAULTPROVIDER_REF", "1")
173
174     def aux():
175         return arnied_wrapper.set_cnf_pipe(cnf, block=block), "", None
176
177     if block is False:
178         succ = aux()
179         return sysmisc.RUN_RESULT_OK if succ is True else sysmisc.RUN_RESULT_FAIL
180
181     res, err = sysmisc.cmd_block_till(aux, DIALTOOLS_TIMEOUT, _connd_online,
182                                       prid=prid)
183     log.debug("arnied_dial_permanent: result (%d, %r)", res, err)
184     return res
185
186
187 def dialout(mode=DIALOUT_MODE_DEFAULT, prid="P1", block=True):
188     """
189
190     :param  mode:   How to dial (permanent vs. manual).
191     :type   mode:   int (``DIALOUT_MODE``) | string
192     :param  prid:   Provider id, default *P1*. It is up to the caller to ensure
193                     this is a valid provider id.
194     :type   prid:   str (constrained by available providers, obviously).
195     :param block:   Whether to block until completion of the command.
196     :type  block:   bool
197     :returns:       Whether the command succeeded.
198     :rtype:         int (dial result)
199     :raises:        :py:class:`ValueError` if invalid dial mode was selected
200     """
201     log.info("go online with provider")
202
203     dmode = None
204     if isinstance(mode, int) is True:
205         dmode = mode
206     elif isinstance(mode, str) is True:
207         try:
208             dmode = DIALOUT_MODE_BY_NAME[mode]
209         except:
210             log.error("invalid online mode name “%s” requested" % mode)
211             pass
212
213     if dmode is None:
214         raise ValueError("exiting due to invalid online mode %r" % mode)
215
216     log.debug("go online, mode=%d(%s), id=%r", dmode, mode, prid)
217
218     if dmode == DIALOUT_MODE_PERMANENT:
219         return arnied_dial_permanent(prid, block=block)
220
221     if dmode == DIALOUT_MODE_MANUAL:
222         return arnied_dial_doc(prid, block=block)
223
224     raise ValueError("invalid dialout mode %r/%r requested" % (mode, dmode))
225
226
227 def get_wan_address(vm=None):
228     """
229     Retrieve the current WAN IP address of client ``vm`` or localhost.
230
231     :param     vm:  Guest (client) to query; will as local connd if left unspecified.
232     :type      vm:  virttest.qemu_vm.VM | None
233
234     :returns:       The IPv4 address. For correctness, it will use the
235                     ipaddress module if available. Otherwise it falls back
236                     on untyped data.
237     :rtype:         None | (ipaddress.IPv4Address | str)
238     """
239     log.info("query current lease")
240     if vm is None:
241         succ, connstat, _ = sysmisc.run_cmd_with_pipe([TELL_CONND_BIN, "--status"])
242         if succ is False:
243             return None
244     else:
245         connstat = vm.session.cmd_output("%s --status" % TELL_CONND_BIN)
246     astr = io.StringIO(connstat)
247
248     while True:
249         l = astr.readline()
250         if l == "":
251             break
252         if l.find("connected online fsm<online> IP:") != -1:
253             addr = l[l.find("IP:")+3:l.find(" )\n")]  # beurk
254             if HAVE_IPADDRESS is True:
255                 return ipaddress.IPv4Address(str(addr))
256             else:
257                 return addr
258     return None