| 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 |