Esempio n. 1
0
    def outlet_update(
        self, upd_linked=False, refresh=False, key="defined", outlets=None
    ):
        """
        Called by consolepi-menu refresh
        """
        pwr = self.pwr
        if config.power:
            outlets = pwr.data if outlets is None else outlets
            if not self.pwr_init_complete or refresh:
                _outlets = pwr.pwr_get_outlets(
                    outlet_data=outlets.get("defined", {}),
                    upd_linked=upd_linked,
                    failures=outlets.get("failures", {}),
                )
                pwr.data = _outlets
            else:
                _outlets = outlets

            if key in _outlets:
                return _outlets[key]
            else:
                msg = (
                    f'Invalid key ({key}) passed to outlet_update. Returning "defined"'
                )
                log.error(msg, show=True)
                return _outlets["defined"]
Esempio n. 2
0
    def get_adapters_via_api(self,
                             ip: str,
                             port: int = 5000,
                             rename: bool = False,
                             log_host: str = None):
        """Send RestFul GET request to Remote ConsolePi to collect adapter info

        params:
        ip(str): ip address or FQDN of remote ConsolePi
        rename(bool): TODO
        log_host(str): friendly string for logging purposes "hostname(ip)"

        returns:
        adapter dict for remote if successful and adapters exist
        status_code 200 if successful but no adapters or Falsey or response status_code if an error occurred.
        """
        if not log_host:
            log_host = ip
        url = f"http://{ip}:{port}/api/v1.0/adapters"
        if rename:
            url = f"{url}?refresh=true"

        log.debug(url)

        headers = {
            "Accept": "*/*",
            "Cache-Control": "no-cache",
            "Host": f"{ip}:{port}",
            "accept-encoding": "gzip, deflate",
            "Connection": "keep-alive",
            "cache-control": "no-cache",
        }

        try:
            response = requests.request("GET",
                                        url,
                                        headers=headers,
                                        timeout=config.remote_timeout)
        except (OSError, TimeoutError):
            log.warning(
                f"[API RQST OUT] Remote ConsolePi: {log_host} TimeOut when querying via API - Unreachable."
            )
            return False

        if response.ok:
            ret = response.json()
            ret = ret["adapters"] if ret["adapters"] else response.status_code
            _msg = f"Adapters Successfully retrieved via API for Remote ConsolePi: {log_host}"
            log.info("[API RQST OUT] {}".format(_msg))
            log.debugv("[API RQST OUT] Response: \n{}".format(
                json.dumps(ret, indent=4, sort_keys=True)))
        else:
            ret = response.status_code
            log.error(
                f"[API RQST OUT] Failed to retrieve adapters via API for Remote ConsolePi: {log_host}\n{ret}:{response.text}"
            )
        return ret
Esempio n. 3
0
    def get_adapters_via_api(self, ip: str, rename: bool = False):
        """Send RestFul GET request to Remote ConsolePi to collect adapter info

        params:
        ip(str): ip address or FQDN of remote ConsolePi

        returns:
        adapter dict for remote if successful
        Falsey or response status_code if an error occured.
        """
        # log = self.config.log
        if rename:
            url = f"http://{ip}:5000/api/v1.0/adapters?refresh=true"
        else:
            url = f"http://{ip}:5000/api/v1.0/adapters"
        log.info(url)  # DEBUG

        headers = {
            "Accept": "*/*",
            "Cache-Control": "no-cache",
            "Host": f"{ip}:5000",
            "accept-encoding": "gzip, deflate",
            "Connection": "keep-alive",
            "cache-control": "no-cache",
        }

        try:
            response = requests.request("GET",
                                        url,
                                        headers=headers,
                                        timeout=config.remote_timeout)
        except (OSError, TimeoutError):
            log.warning(
                "[API RQST OUT] Remote ConsolePi @ {} TimeOut when querying via API - Unreachable."
                .format(ip))
            return False

        if response.ok:
            ret = response.json()
            ret = ret["adapters"] if ret["adapters"] else response.status_code
            _msg = "Adapters Successfully retrieved via API for Remote ConsolePi @ {}".format(
                ip)
            log.info("[API RQST OUT] {}".format(_msg))
            log.debugv("[API RQST OUT] Response: \n{}".format(
                json.dumps(ret, indent=4, sort_keys=True)))
        else:
            ret = response.status_code
            log.error(
                "[API RQST OUT] Failed to retrieve adapters via API for Remote ConsolePi @ {}\n{}:{}"
                .format(ip, ret, response.text))
        return ret
Esempio n. 4
0
 def exec_request(self, _request):
     result = None
     attempt = 0
     while True:
         attempt += 1
         try:
             result = _request.execute()
             break
         except Exception as e:
             log.error(('[GDRIVE]: Exception while communicating with Gdrive\n {}'.format(e)))
         if attempt > 2:
             log.error(('[GDRIVE]: Giving up after {} attempts'.format(attempt)))
             break
     return result
Esempio n. 5
0
 def auth(self):
     if utils.is_reachable('www.googleapis.com', 443):
         try:
             if self.creds is None:
                 self.creds = self.get_credentials()
             if self.sheets_svc is None:
                 self.sheets_svc = discovery.build('sheets', 'v4', credentials=self.creds, cache_discovery=False)
             if self.file_id is None:
                 self.file_id = self.get_file_id()
                 if self.file_id is None:
                     self.file_id = self.create_sheet()
             return True
         except (ConnectionError, TimeoutError, OSError) as e:
             log.error('Exception Occurred Connecting to Gdrive {}'.format(e))
             return False
     else:
         log.error('Google Drive is not reachable - Aborting')
         return False
Esempio n. 6
0
    def wait_for_threads(self, name="init", timeout=10, thread_type="power"):
        """wait for parallel async threads to complete

        returns:
            bool: True if threads are still running indicating a timeout
                  None indicates no threads found ~ they have finished
        """
        start = time.time()
        do_log = False
        found = False
        while True:
            found = False
            for t in threading.enumerate():
                if name in t.name:
                    found = do_log = True
                    t.join(timeout - 1)

            if not found:
                if name == "init" and thread_type == "power":
                    if self.pwr and not self.pwr.data or not self.pwr.data.get(
                            "dli_power"):
                        self.pwr.dli_exists = False
                    self.pwr_init_complete = True
                if do_log:
                    log.info(
                        "[{0} {1} WAIT] {0} Threads have Completed, elapsed time: {2}"
                        .format(
                            name.strip("_").upper(),
                            thread_type.upper(),
                            time.time() - start,
                        ))
                break
            elif time.time() - start > timeout:
                log.error(
                    "[{0} {1} WAIT] Timeout Waiting for {0} Threads to Complete, elapsed time: {2}"
                    .format(
                        name.strip("_").upper(),
                        thread_type.upper(),
                        time.time() - start,
                    ),
                    show=True,
                )
                return True
Esempio n. 7
0
    def do_api_request(self, ip: str, path: str, *args, **kwargs):
        """Send RestFul GET request to Remote ConsolePi to collect data

        params:
        ip(str): ip address or FQDN of remote ConsolePi
        path(str): path beyond /api/v1.0/

        returns:
        response object
        """
        url = f"http://{ip}:5000/api/v1.0/{path}"
        log.debug(f'[do_api_request] URL: {url}')

        headers = {
            "Accept": "*/*",
            "Cache-Control": "no-cache",
            "Host": f"{ip}:5000",
            "accept-encoding": "gzip, deflate",
            "Connection": "keep-alive",
            "cache-control": "no-cache",
        }

        try:
            response = requests.request("GET",
                                        url,
                                        headers=headers,
                                        timeout=config.remote_timeout)
        except (OSError, TimeoutError):
            log.warning(
                f"[API RQST OUT] Remote ConsolePi @ {ip} TimeOut when querying via API - Unreachable."
            )
            return False

        if response.ok:
            log.info(f"[API RQST OUT] {url} Response: OK")
            log.debugv(
                f"[API RQST OUT] Response: \n{json.dumps(response.json(), indent=4, sort_keys=True)}"
            )
        else:
            log.error(f"[API RQST OUT] API Request Failed {url}")

        return response
Esempio n. 8
0
    def run(self, connection_args):
        '''Establish Connection to device and run CLI commands provided via ztp config

        Args:
            connection_args (dict): Arguments passed to paramiko to establish connection
        '''
        result = dict(
            changed=False,
            cli_output=[],
            message=''
        )
        _start_time = time.time()
        while True:
            try:
                go = False
                # Connect to Switch via SSH
                self.ssh_client.connect(**connection_args)
                self.prompt = ''
                # SSH Command execution not allowed, therefore using the following paramiko functionality
                self.shell_chanel = self.ssh_client.invoke_shell()
                self.shell_chanel.settimeout(8)
                # AOS-CX specific
                self.get_prompt()
                go = True
                break
            except socket.timeout:
                log.error(f'ZTP CLI Operations Failed, TimeOut Connecting to {self.ip}')
            except paramiko.ssh_exception.NoValidConnectionsError as e:
                log.error(f'ZTP CLI Operations Failed, {e}')
            except paramiko.ssh_exception.AuthenticationException:
                log.error('ZTP CLI Operations Failed, CLI Authentication Failed verify creds in config')

            if time.time() - _start_time >= ZTP_CLI_LOGIN_MAX_WAIT:
                break  # Give Up
            else:
                time.sleep(10)

        if go:
            try:
                result['cli_output'] = self.execute_command(self.cmd_list)
                result['changed'] = True
                if self.fail_msg:
                    result['message'] += self.fail_msg.get('msg')
            finally:
                self.logout()

            # Format log entries and exit
            _res = " -- // Command Results \\ -- \n"
            _cmds = [c for c in self.cmd_list if 'SLEEP' not in c]
            for cmd, out in zip(_cmds, result['cli_output']):
                if "progress:" in out and f"progress: {out.count('progress:')}/{out.count('progress:')}" in out:
                    out = out.split("progress:")[0] + f"progress: {out.count('progress:')}/{out.count('progress:')}"
                _res += "{}:{} {}\n".format(cmd, '\n' if '\n' in out else '', out)
            _res += " --------------------------- \n"
            _res += ''.join([f"{k}: {v}\n" for k, v in result.items() if k != "cli_output" and v])
            log.info(f"Post ZTP CLI Operational Result for {ip}:\n{_res}")
Esempio n. 9
0
def main():
    cpi = ConsolePi()
    cloud_svc = config.cfg.get("cloud_svc", "error")
    local = cpi.local
    remotes = cpi.remotes
    cpiexec = cpi.cpiexec
    log.info('[CLOUD TRIGGER (IP)]: Cloud Update triggered by IP Update')
    CLOUD_CREDS_FILE = config.static.get(
        "CLOUD_CREDS_FILE",
        '/etc/ConsolePi/cloud/gdrive/.credentials/credentials.json')
    if not utils.is_reachable("www.googleapis.com", 443):
        log.error(f"Not Updating {cloud_svc} due to connection failure")
        sys.exit(1)
        if not utils.valid_file(CLOUD_CREDS_FILE):
            log.error('Credentials file not found or invalid')
            sys.exit(1)

    # -- // Get details from Google Drive - once populated will skip \\ --
    if cloud_svc == "gdrive" and remotes.cloud is None:
        remotes.cloud = GoogleDrive(hostname=local.hostname)

    if cpiexec.wait_for_threads(thread_type="remotes") and (
            config.power and cpiexec.wait_for_threads(name="_toggle_refresh")):
        log.error(
            'IP Change Cloud Update Trigger: TimeOut Waiting for Threads to Complete'
        )

    remote_consoles = remotes.cloud.update_files(local.data)
    if remote_consoles and "Gdrive-Error:" in remote_consoles:
        log.error(remote_consoles)
    else:
        for r in remote_consoles:
            # -- Convert Any Remotes with old API schema to new API schema --
            if isinstance(remote_consoles[r].get("adapters", {}), list):
                remote_consoles[r]["adapters"] = remotes.convert_adapters(
                    remote_consoles[r]["adapters"])
                log.warning(
                    f"Adapter data for {r} retrieved from cloud in old API format... Converted"
                )
        if len(remote_consoles) > 0:
            remotes.update_local_cloud_file(remote_consoles)
Esempio n. 10
0
    def get_remote(self, data=None, rename=False):
        spin = self.spin

        def verify_remote_thread(remotepi, data, rename):
            """sub to verify reachability and api data for remotes

            params:
            remotepi: The hostname currently being processed
            data: dict remote ConsolePi dict with hostname as key
            """
            this = data[remotepi]
            res = self.api_reachable(remotepi, this, rename=rename)
            this = res.data
            if res.update:
                self.cache_update_pending = True

            if not res.reachable:
                log.warning(
                    f"[GET REM] Found {remotepi} in Local Cloud Cache: UNREACHABLE"
                )
                this["fail_cnt"] = (1 if not this.get("fail_cnt") else
                                    this["fail_cnt"] + 1)
                self.pop_list.append(remotepi)
                self.cache_update_pending = True
            else:
                self.connected = True
                if this.get("fail_cnt"):
                    this["fail_cnt"] = 0
                    self.cache_update_pending = True
                if res.update:
                    log.info(
                        f"[GET REM] Updating Cache - Found {remotepi} in Local Cloud Cache, "
                        f"reachable via {this['rem_ip']}")

            data[remotepi] = this

        if data is None or len(data) == 0:
            data = config.remotes  # remotes from local cloud cache

        if not data:
            # print(self.log_sym_warn + " No Remotes in Local Cache")
            log.info("No Remotes found in Local Cache")
            data = {}  # convert None type to empy dict
        else:
            # if self is in the remote-data remove and warn user (can occur in rare scenarios i.e. hostname changes)
            if socket.gethostname() in data:
                del data[socket.gethostname()]
                log.show(
                    "Local cache included entry for self - do you have other ConsolePis using the same hostname?"
                )

            # Verify Remote ConsolePi details and reachability
            if stdin.isatty():
                spin.start(
                    "Querying Remotes via API to verify reachability and adapter data"
                )
            for remotepi in data:
                # -- // Launch Threads to verify all remotes in parallel \\ --
                threading.Thread(
                    target=verify_remote_thread,
                    args=(remotepi, data, rename),
                    name=f"vrfy_{remotepi}",
                ).start()
                # verify_remote_thread(remotepi, data)  # Non-Threading DEBUG

            # -- wait for threads to complete --
            if not self.cpiexec.wait_for_threads(name="vrfy_",
                                                 thread_type="remote"):
                if config.remotes:
                    if stdin.isatty():
                        spin.succeed(
                            "[GET REM] Querying Remotes via API to verify reachability and adapter data\n\t"
                            f"Found {len(config.remotes)} Remote ConsolePis")
                else:
                    if stdin.isatty():
                        spin.warn(
                            "[GET REM] Querying Remotes via API to verify reachability and adapter data\n\t"
                            "No Reachable Remote ConsolePis Discovered")
            else:
                log.error(
                    "[GET REM] Remote verify threads Still running / exceeded timeout"
                )
                if stdin.isatty():
                    spin.stop()

        # update local cache if any ConsolePis found UnReachable
        if self.cache_update_pending:
            if self.pop_list:
                for remotepi in self.pop_list:
                    if (
                            data[remotepi]["fail_cnt"] >= 3
                    ):  # NoQA remove from local cache after 3 failures (cloud or mdns will repopulate if discovered)
                        removed = data.pop(remotepi)
                        log.warning(
                            "[GET REM] {} has been removed from Local Cache after {} failed attempts"
                            .format(remotepi, removed["fail_cnt"]),
                            show=True,
                        )
                    else:
                        log.show("Cached Remote '{}' is unreachable".format(
                            remotepi))

            # update local cache file if rem_ip or adapter data changed
            data = self.update_local_cloud_file(data)
            self.pop_list = []
            self.cache_update_pending = False

        return data
Esempio n. 11
0
    def do_rename_adapter(self, from_name):
        '''Rename USB to Serial Adapter

        Creates new or edits existing udev rules and ser2net conf
        for USB to serial adapters detected by the system.

        params:
        from_name(str): Devices current name passed in from rename_menu()

        returns:
        None type if no error, or Error (str) if Error occurred
        '''
        from_name = from_name.replace('/dev/', '')
        local = self.cpi.local
        c = {
            'green': '\033[1;32m',  # Bold with normal ForeGround
            'red': '\033[1;31m',
            'norm': '\033[0m',  # Reset to Normal
        }
        c_from_name = '{}{}{}'.format(c['red'], from_name, c['norm'])
        error = False
        use_def = True

        try:
            to_name = None
            while not to_name:
                print(
                    " Press 'enter' to keep the same name and change baud/parity/..."
                )
                to_name = input(
                    f' [rename {c_from_name}]: Provide desired name: ')
                print("")
                to_name = to_name or from_name
            to_name = to_name.replace(
                '/dev/',
                '')  # strip /dev/ if they thought they needed to include it
            # it's ok to essentialy rename with same name (to chg baud etc.), but not OK to rename to a name that is already
            # in use by another adapter
            # TODO collect not connected adapters as well to avoid dups
            if from_name != to_name and f"/dev/{to_name}" in local.adapters:
                return f"There is already an adapter using alias {to_name}"

            for _name in self.reserved_names:
                if to_name.startswith(_name):
                    return f"You can't start the alias with {_name}.  Matches system root device prefix"

            if ' ' in to_name or ':' in to_name or '(' in to_name or ')' in to_name:
                print(
                    '\033[1;33m!!\033[0m Spaces, Colons and parentheses are not allowed by the associated config files.\n'
                    '\033[1;33m!!\033[0m Swapping with valid characters\n')
                to_name = to_name.replace(' ', '_').replace('(', '_').replace(
                    ')', '_')  # not allowed in udev
                to_name = to_name.replace(
                    ':', '-'
                )  # replace any colons with - as it's the field delim in ser2net

        except (KeyboardInterrupt, EOFError):
            return 'Rename Aborted based on User Input'

        c_to_name = f'{c["green"]}{to_name}{c["norm"]}'
        log_c_to_name = "".join(["{{green}}", to_name, "{{norm}}"])

        go, con_only = True, False
        if from_name == to_name:
            log.show(
                f"Keeping {log_c_to_name}. Changing connection settings Only.")
            con_only = True
            use_def = False
        elif utils.user_input_bool(' Please Confirm Rename {} --> {}'.format(
                c_from_name, c_to_name)) is False:
            go = False

        if go:
            for i in local.adapters:
                if i == f'/dev/{from_name}':
                    break
            _dev = local.adapters[i].get('config')  # type: ignore # dict
            # -- these values are always safe, values set by config.py if not extracted from ser2net.conf
            baud = _dev['baud']
            dbits = _dev['dbits']
            flow = _dev['flow']
            sbits = _dev['sbits']
            parity = _dev['parity']
            word = 'keep existing'
            for _name in self.reserved_names:
                if from_name.startswith(_name):
                    word = 'Use default'

            # -- // Ask user if they want to update connection settings \\ --
            if not con_only:
                use_def = utils.user_input_bool(
                    ' {} connection values [{} {}{}1 Flow: {}]'.format(
                        word, baud, dbits, parity.upper(),
                        self.flow_pretty[flow]))

            if not use_def:
                self.con_menu(rename=True,
                              con_dict={
                                  'baud': baud,
                                  'data_bits': dbits,
                                  'parity': parity,
                                  'flow': flow,
                                  'sbits': sbits
                              })
                baud = self.baud
                parity = self.parity
                dbits = self.data_bits
                parity = self.parity
                flow = self.flow
                sbits = self.sbits

            # restore defaults back to class attribute if we flipped them when we called con_menu
            # TODO believe this was an old hack, and can be removed
            if hasattr(self, 'con_dict') and self.con_dict:
                self.baud = self.con_dict['baud']
                self.data_bits = self.con_dict['data_bits']
                self.parity = self.con_dict['parity']
                self.flow = self.con_dict['flow']
                self.sbits = self.con_dict['sbits']
                self.con_dict = None

            if word == 'Use default':  # see above word is set if from_name matches a root_dev pfx
                devs = local.detect_adapters()
                if f'/dev/{from_name}' in devs:
                    _tty = devs[f'/dev/{from_name}']
                    id_prod = _tty.get('id_model_id')
                    id_model = _tty.get('id_model')  # NoQA pylint: disable=unused-variable
                    id_vendorid = _tty.get('id_vendor_id')
                    id_vendor = _tty.get('id_vendor')  # NoQA pylint: disable=unused-variable
                    id_serial = _tty.get('id_serial_short')
                    id_ifnum = _tty.get('id_ifnum')
                    id_path = _tty.get('id_path')  # NoQA
                    lame_devpath = _tty.get('lame_devpath')
                    root_dev = _tty.get('root_dev')
                else:
                    return 'ERROR: Adapter no longer found'

                # -- // ADAPTERS WITH ALL ATTRIBUTES AND GPIO UART (TTYAMA) \\ --
                if id_prod and id_serial and id_vendorid:
                    if id_serial not in devs['_dup_ser']:
                        udev_line = (
                            'ATTRS{{idVendor}}=="{}", ATTRS{{idProduct}}=="{}", '
                            'ATTRS{{serial}}=="{}", SYMLINK+="{}"'.format(
                                id_vendorid, id_prod, id_serial, to_name))

                        error = None
                        while not error:
                            error = self.add_to_udev(udev_line,
                                                     '# END BYSERIAL-DEVS')
                            error = self.do_ser2net_line(from_name=from_name,
                                                         to_name=to_name,
                                                         baud=baud,
                                                         dbits=dbits,
                                                         parity=parity,
                                                         flow=flow)
                            break

                    # -- // MULTI-PORT ADAPTERS WITH COMMON SERIAL (different ifnums) \\ --
                    else:
                        # SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6011", ATTRS{serial}=="FT4XXXXP", GOTO="FTXXXXP"  # NoQA
                        udev_line = (
                            'ATTRS{{idVendor}}=="{0}", ATTRS{{idProduct}}=="{1}", '
                            'ATTRS{{serial}}=="{2}", GOTO="{2}"'.format(
                                id_vendorid, id_prod, id_serial))

                        error = None
                        while not error:
                            error = self.add_to_udev(udev_line,
                                                     '# END BYPORT-POINTERS')
                            # ENV{ID_USB_INTERFACE_NUM}=="00", SYMLINK+="FT4232H_port1", GOTO="END"
                            udev_line = (
                                'ENV{{ID_USB_INTERFACE_NUM}}=="{}", SYMLINK+="{}"'
                                .format(id_ifnum, to_name))
                            error = self.add_to_udev(udev_line,
                                                     '# END BYPORT-DEVS',
                                                     label=id_serial)
                            error = self.do_ser2net_line(from_name=from_name,
                                                         to_name=to_name,
                                                         baud=baud,
                                                         dbits=dbits,
                                                         parity=parity,
                                                         flow=flow)
                            break

                else:
                    if f'/dev/{from_name}' in devs:
                        devname = devs[f'/dev/{from_name}'].get('devname', '')
                        # -- // local ttyAMA adapters \\ --
                        if 'ttyAMA' in devname:
                            udev_line = ('KERNEL=="{}", SYMLINK+="{}"'.format(
                                devname.replace('/dev/', ''), to_name))

                            # Testing simplification not using separate file for ttyAMA
                            error = None
                            while not error:
                                error = self.add_to_udev(
                                    udev_line, '# END TTYAMA-DEVS')
                                error = self.do_ser2net_line(
                                    from_name=from_name,
                                    to_name=to_name,
                                    baud=baud,
                                    dbits=dbits,
                                    parity=parity,
                                    flow=flow)
                                break
                        else:
                            # -- // LAME ADAPTERS NO SERIAL NUM (map usb port) \\ --
                            log.warning(
                                '[ADD ADAPTER] Lame adapter missing key detail: idVendor={}, idProduct={}, serial#={}'
                                .format(  # NoQA
                                    id_vendorid, id_prod, id_serial))
                            print(
                                '\n\n This Device Does not present a serial # (LAME!).  So the adapter itself can\'t be '
                                'uniquely identified.\n There are 2 options for naming this device:'
                            )

                            mlines = [
                                '1. Map it to the USB port it\'s plugged in to'
                                '\n\tAnytime a {} {} tty device is plugged into the port it\n\tis currently plugged into it will '
                                'adopt the {} alias'.format(
                                    _tty['id_vendor_from_database'],
                                    _tty['id_model_from_database'], to_name),
                                '2. Map it by vedor ({0}) and model ({1}) alone.'
                                '\n\tThis will only work if this is the only {0} {1} adapter you plan to plug in'
                                .format(_tty['id_vendor_from_database'],
                                        _tty['id_model_from_database'])
                                # 'Temporary mapping' \
                                # '\n\tnaming will only persist during this menu session\n'
                            ]
                            print(self.menu.format_subhead(mlines))
                            print('\n b. back (abort rename)\n')
                            valid_ch = {'1': 'by_path', '2': 'by_id'}
                            valid = False
                            ch = ''
                            while not valid:
                                print(' Please Select an option')
                                ch = self.wait_for_input()
                                if ch.lower == 'b':
                                    log.show(
                                        f'Rename {from_name} --> {to_name} Aborted'
                                    )
                                    return
                                elif ch.lower in valid_ch:
                                    valid = True
                                else:
                                    print(
                                        'invalid choice {} Try Again.'.format(
                                            ch.orig))

                            udev_line = None
                            if valid_ch[ch.lower] == 'temp':
                                error = True
                                print(
                                    'The Temporary rename feature is not yet implemented'
                                )
                            elif valid_ch[ch.lower] == 'by_path':
                                udev_line = (
                                    'ATTRS{{idVendor}}=="{0}", ATTRS{{idProduct}}=="{1}", GOTO="{0}_{1}"'.format(  # NoQA
                                        id_vendorid, id_prod), 'ATTRS{{devpath}}=="{}", ENV{{ID_USB_INTERFACE_NUM}}=="{}", '\
                                                               'SYMLINK+="{}"'.format(lame_devpath, id_ifnum, to_name),
                                )
                            elif valid_ch[ch.lower] == 'by_id':
                                udev_line = (
                                    'SUBSYSTEM=="tty", ATTRS{{idVendor}}=="{0}", ATTRS{{idProduct}}=="{1}", GOTO="{0}_{1}"'
                                    .format(  # NoQA
                                        id_vendorid, id_prod),
                                    'ENV{{ID_USB_INTERFACE_NUM}}=="{}", SYMLINK+="{}", GOTO="END"'
                                    .format(id_ifnum, to_name)  # NoQA
                                )
                            else:
                                error = [
                                    'Unable to add udev rule adapter missing details',
                                    'idVendor={}, idProduct={}, serial#={}'.
                                    format(  # NoQA
                                        id_vendorid, id_prod, id_serial)
                                ]

                            while udev_line:
                                error = self.add_to_udev(
                                    udev_line[0], '# END BYPATH-POINTERS')
                                error = self.add_to_udev(udev_line[1],
                                                         '# END BYPATH-DEVS',
                                                         label='{}_{}'.format(
                                                             id_vendorid,
                                                             id_prod))  # NoQA
                                error = self.do_ser2net_line(
                                    from_name=from_name,
                                    to_name=to_name,
                                    baud=baud,
                                    dbits=dbits,
                                    parity=parity,
                                    flow=flow)
                                break
                    else:
                        log.error(f'Device {from_name} No Longer Found',
                                  show=True)

            # TODO simplify once ser2net existing verified
            else:  # renaming previously named port.
                # -- // local ttyAMA adapters \\ --
                devname = local.adapters[f'/dev/{from_name}']['udev'].get(
                    'devname', '')
                rules_file = self.rules_file if 'ttyAMA' not in devname else self.ttyama_rules_file

                cmd = 'sudo sed -i "s/{0}{3}/{1}{3}/g" {2} && grep -q "{1}{3}" {2} && [ $(grep -c "{0}{3}" {2}) -eq 0 ]'.format(
                    from_name, to_name, rules_file, '')
                error = utils.do_shell_cmd(cmd, shell=True)
                if not error:
                    error = self.do_ser2net_line(from_name=from_name,
                                                 to_name=to_name,
                                                 baud=baud,
                                                 dbits=dbits,
                                                 parity=parity,
                                                 flow=flow)
                else:
                    return [
                        error.split('\n'),
                        'Failed to change {} --> {} in {}'.format(
                            from_name, to_name, self.ser2net_file)
                    ]

            if not error:
                # Update adapter variables with new_name
                local.adapters[f'/dev/{to_name}'] = local.adapters[
                    f'/dev/{from_name}']
                local.adapters[f'/dev/{to_name}']['config'][
                    'port'] = config.ser2net_conf[f'/dev/{to_name}'].get(
                        'port', 0)
                local.adapters[f'/dev/{to_name}']['config'][
                    'cmd'] = config.ser2net_conf[f'/dev/{to_name}'].get('cmd')
                local.adapters[f'/dev/{to_name}']['config'][
                    'line'] = config.ser2net_conf[f'/dev/{to_name}'].get(
                        'line')
                local.adapters[f'/dev/{to_name}']['config'][
                    'log'] = config.ser2net_conf[f'/dev/{to_name}'].get('log')
                local.adapters[f'/dev/{to_name}']['config'][
                    'log_ptr'] = config.ser2net_conf[f'/dev/{to_name}'].get(
                        'log_ptr')
                _config_dict = local.adapters[f'/dev/{to_name}']['config']
                if not use_def:  # overwrite con settings if they were changed
                    updates = {
                        'baud': baud,
                        'dbits': dbits,
                        'flow': flow,
                        'parity': parity,
                        'sbits': sbits,
                    }
                    local.adapters[f'/dev/{to_name}']['config'] = {
                        **_config_dict,
                        **updates
                    }

                if from_name != to_name:  # facilitates changing con settings without actually renaming
                    del local.adapters[f'/dev/{from_name}']

                self.udev_pending = True  # toggle for exit function if they exit directly from rename memu

                # update first item in first section of menu_body menu uses it to determine if section is a continuation
                try:
                    self.cur_menu.body_in[0][0] = self.cur_menu.body_in[0][
                        0].replace(from_name, to_name)
                    if self.menu.body_in is not None:  # Can be none when called via rename directly
                        self.menu.body_in[0][0] = self.menu.body_in[0][
                            0].replace(from_name, to_name)
                except Exception as e:
                    log.exception(
                        f"[DEV NOTE menu_body update after rename caused exception.\n{e}",
                        show=False)

        else:
            return 'Aborted based on user input'
Esempio n. 12
0
    def pwr_get_outlets(self, outlet_data={}, upd_linked=False, failures={}):
        '''Get Details for Outlets defined in ConsolePi.yaml power section

        On Menu Launch this method is called in parallel (threaded) for each outlet
        On Refresh all outlets are passed to the method

        params: - All Optional
            outlet_data:dict, The outlets that need to be updated, if not provided will get all outlets defined in ConsolePi.yaml
            upd_linked:Bool, If True will update just the linked ports, False is for dli and will update
                all ports for the dli.
            failures:dict: when refreshing outlets pass in previous failures so they can be re-tried
        '''
        # re-attempt connection to failed power controllers on refresh
        if not failures:
            failures = outlet_data.get('failures') if outlet_data.get('failures') else self.data.get('failures')

        outlet_data = self.data.get('defined') if not outlet_data else outlet_data
        if failures:
            outlet_data = {**outlet_data, **failures}
            failures = {}

        dli_power = self.data.get('dli_power', {})

        for k in outlet_data:
            outlet = outlet_data[k]
            _start = time.time()
            # -- // GPIO \\ --
            if outlet['type'].upper() == 'GPIO':
                if not is_rpi:
                    log.warning('GPIO Outlet Defined, GPIO Only Supported on RPi - ignored', show=True)
                    continue
                noff = True if 'noff' not in outlet else outlet['noff']
                GPIO.setup(outlet['address'], GPIO.OUT)
                outlet_data[k]['is_on'] = bool(GPIO.input(outlet['address'])) if noff \
                    else not bool(GPIO.input(outlet['address']))

            # -- // tasmota \\ --
            elif outlet['type'] == 'tasmota':
                response = self.do_tasmota_cmd(outlet['address'])
                outlet['is_on'] = response
                if response not in [0, 1, True, False]:
                    failures[k] = outlet_data[k]
                    failures[k]['error'] = f'[PWR-TASMOTA] {k}:{failures[k]["address"]} {response} - Removed'
                    log.warning(failures[k]['error'], show=True)

            # -- // esphome \\ --
            elif outlet['type'] == 'esphome':
                # TODO have do_esphome accept list, slice, or str for one or multiple relays
                relays = utils.listify(outlet.get('relays', k))  # if they have not specified the relay try name of outlet
                outlet['is_on'] = {}
                for r in relays:
                    response = self.do_esphome_cmd(outlet['address'], r)
                    outlet['is_on'][r] = {'state': response, 'name': r}
                    if response not in [True, False]:
                        failures[k] = outlet_data[k]
                        failures[k]['error'] = f'[PWR-ESP] {k}:{failures[k]["address"]} {response} - Removed'
                        log.warning(failures[k]['error'], show=True)

            # -- // dli \\ --
            elif outlet['type'].lower() == 'dli':
                if TIMING:
                    dbg_line = '------------------------ // NOW PROCESSING {} \\\\ ------------------------'.format(k)
                    print('\n{}'.format('=' * len(dbg_line)))
                    print('{}\n{}\n{}'.format(dbg_line, outlet_data[k], '-' * len(dbg_line)))
                    print('{}'.format('=' * len(dbg_line)))

                # -- // VALIDATE CONFIG FILE DATA FOR DLI \\ --
                all_good = True  # initial value
                for _ in ['address', 'username', 'password']:
                    if not outlet.get(_):
                        all_good = False
                        failures[k] = outlet_data[k]
                        failures[k]['error'] = f'[PWR-DLI {k}] {_} missing from {failures[k]["address"]} ' \
                            'configuration - skipping'
                        log.error(f'[PWR-DLI {k}] {_} missing from {failures[k]["address"]} '
                                  'configuration - skipping', show=True)
                        break
                if not all_good:
                    continue

                (this_dli, _update) = self.load_dli(outlet['address'], outlet['username'], outlet['password'])
                if this_dli is None or this_dli.dli is None:
                    failures[k] = outlet_data[k]
                    failures[k]['error'] = '[PWR-DLI {}] {} Unreachable - Removed'.format(k, failures[k]['address'])
                    log.warning(f"[PWR-DLI {k}] {failures[k]['address']} Unreachable - Removed", show=True)
                else:
                    if TIMING:
                        xstart = time.time()
                        print('this_dli.outlets: {} {}'.format(this_dli.outlets, 'update' if _update else 'init'))
                        print(json.dumps(dli_power, indent=4, sort_keys=True))

                    # upd_linked is for faster update in power menu only refreshes data for linked ports vs entire dli
                    if upd_linked and self.data['dli_power'].get(outlet['address']):
                        if outlet.get('linked_devs'):
                            (outlet, _p) = self.update_linked_devs(outlet)
                            if k in outlet_data:
                                outlet_data[k]['is_on'] = this_dli[_p]
                            else:
                                log.error(f'[PWR GET_OUTLETS] {k} appears to be unreachable')

                            # TODO not actually using the error returned this turned into a hot mess
                            if isinstance(outlet['is_on'], dict) and not outlet['is_on']:
                                all_good = False
                            # update dli_power for the refreshed / linked ports
                            else:
                                for _ in outlet['is_on']:
                                    dli_power[outlet['address']][_] = outlet['is_on'][_]
                    else:
                        if _update:
                            dli_power[outlet['address']] = this_dli.get_dli_outlets()  # data may not be fresh trigger dli update

                            # handle error connecting to dli during refresh - when connect worked on menu launch
                            if not dli_power[outlet['address']]:
                                failures[k] = outlet_data[k]
                                failures[k]['error'] = f"[PWR-DLI] {k} {failures[k]['address']} Unreachable - Removed"
                                log.warning(f'[PWR-DLI {k}] {failures[k]["address"]} Unreachable - Removed',
                                            show=True)
                                continue
                        else:  # dli was just instantiated data is fresh no need to update
                            dli_power[outlet['address']] = this_dli.outlets

                        if outlet.get('linked_devs'):
                            (outlet, _p) = self.update_linked_devs(outlet)

                if TIMING:
                    print('[TIMING] this_dli.outlets: {}'.format(time.time() - xstart))  # TIMING

            log.debug(f'dli {k} Updated. Elapsed Time(secs): {time.time() - _start}')
            # -- END for LOOP for k in outlet_data --

        # Move failed outlets from the keys that populate the menu to the 'failures' key
        # failures are displayed in the footer section of the menu, then re-tried on refresh
        # TODO this may be causing - RuntimeError: dictionary changed size during iteration
        # in pwr_start_update_threads. witnessed on mdnsreg daemon on occasion (Move del logic after wait_for_threads?)
        for _dev in failures:
            if outlet_data.get(_dev):
                del outlet_data[_dev]
            if self.data['defined'].get(_dev):
                del self.data['defined'][_dev]
            if failures[_dev]['address'] in dli_power:
                del dli_power[failures[_dev]['address']]
            self.data['failures'][_dev] = failures[_dev]

        # restore outlets that failed on menu launch but found reachable during refresh
        for _dev in outlet_data:
            if _dev not in self.data['defined']:
                self.data['defined'][_dev] = outlet_data[_dev]
            if _dev in self.data['failures']:
                del self.data['failures'][_dev]

        self.data['dli_power'] = dli_power

        return self.data
Esempio n. 13
0
    def detect_adapters(self, key=None):
        """Detect Locally Attached Adapters.

        Returns
        -------
        dict
            udev alias/symlink if defined/found as key or root device if not.
            /dev/ is stripped: (ttyUSB0 | AP515).  Each device has it's attrs
            in a dict.
        """
        if key is not None:
            key = '/dev/' + key.split('/')[
                -1]  # key can be provided with or without /dev/ prefix

        context = pyudev.Context()

        devs = {'_dup_ser': {}}
        usb_list = [
            dev.properties['DEVPATH'].split('/')[-1]
            for dev in context.list_devices(ID_BUS='usb', subsystem='tty')
        ]
        pci_list = [
            dev.properties['DEVPATH'].split('/')[-1]
            for dev in context.list_devices(ID_BUS='pci', subsystem='tty')
        ]
        ama_list = [
            dev.replace('/dev/', '')
            for dev in config.cfg_yml.get('TTYAMA', {})
        ]
        root_dev_list = usb_list + pci_list + ama_list

        for root_dev in root_dev_list:
            # determine if the device already has a udev alias & collect available path options for use on lame adapters
            dev_name = by_path = by_id = None
            try:
                _dev = pyudev.Devices.from_name(context, 'tty', root_dev)
            except pyudev._errors.DeviceNotFoundByNameError:
                log.error(f'pyudev Ubable to find {root_dev}')
                continue  # TODO Catching error as have seen it in consolepi-mdnsreg not sure if continue is appropriate
            _devlinks = _dev.get('DEVLINKS', '').split()
            if not _devlinks:  # skip occurs on non rpi and ttyAMA
                if not root_dev.startswith('ttyAMA'):
                    continue
            else:
                for _d in _devlinks:
                    if '/dev/serial' not in _d:
                        dev_name = _d.replace('/dev/', '')
                    elif '/dev/serial/by-path/' in _d:
                        by_path = _d
                    elif '/dev/serial/by-id/' in _d:
                        by_id = _d

            dev_name = f'/dev/{root_dev}' if not dev_name else f'/dev/{dev_name}'
            devs[dev_name] = {'by_path': by_path, 'by_id': by_id}
            devs[dev_name][
                'root_dev'] = True if dev_name == f'/dev/{root_dev}' else False

            # Gather all available properties from device
            _props = {
                p.lower() if p != 'ID_USB_INTERFACE_NUM' else 'id_ifnum':
                _dev.properties[p]
                for p in _dev.properties
            }
            devs[dev_name] = {**devs[dev_name], **_props}

            # -- no need for remaining logic on ttyAMA adapters (local UART)
            if 'ttyAMA' in root_dev:
                # TODO clean up logic this is copy paste from below
                # clean up some redundant or less useful properties
                rm_list = [
                    'devlinks', 'id_mm_candidate', 'id_model_enc',
                    'id_path_tag', 'tags', 'major', 'minor',
                    'usec_initialized', 'id_vendor_enc',
                    'id_pci_interface_from_database', 'id_revision'
                ]

                devs[dev_name] = {
                    k: v
                    for k, v in devs[dev_name].items() if k not in rm_list
                }

                continue

            # with some multi-port adapters the model_id and vendor_id need to be pulled from higher in stack
            this_dev = _dev
            while '0x' in this_dev.properties.get(
                    'ID_MODEL_ID', '0x') and hasattr(this_dev, 'parent'):
                this_dev = this_dev.parent

            # -- Collect path for mapping to specific USB port
            # TODO clean this up not efficient could combine search for ID_MODEL_ID and devpath
            lame_devpath = this_dev.attributes.get('devpath')
            if lame_devpath and isinstance(lame_devpath, bytes):
                lame_devpath = lame_devpath.decode('UTF-8')
            else:
                for p in _dev.ancestors:
                    if 'devpath' in p.attributes.available_attributes:
                        lame_devpath = p.attributes.get('devpath')
                        if lame_devpath and isinstance(lame_devpath, bytes):
                            lame_devpath = lame_devpath.decode('UTF-8')
                            break

            devs[dev_name]['lame_devpath'] = lame_devpath

            fallback_ser = this_dev.properties.get('ID_SERIAL_SHORT')
            devs[dev_name]['id_model_id'] = this_dev.properties['ID_MODEL_ID']
            devs[dev_name]['id_vendor_id'] = this_dev.properties[
                'ID_VENDOR_ID']
            devs[dev_name]['time_since_init'] = f'{_dev.properties.device.time_since_initialized} ' \
                                                f"as of {time.strftime('%x %I:%M:%S %p %Z', time.localtime(time.time()))}"

            # clean up some redundant or less useful properties
            rm_list = [
                'devlinks', 'id_mm_candidate', 'id_model_enc', 'id_path_tag',
                'tags', 'major', 'minor', 'usec_initialized', 'id_vendor_enc',
                'id_pci_interface_from_database', 'id_revision'
            ]

            devs[dev_name] = {
                k: v
                for k, v in devs[dev_name].items() if k not in rm_list
            }

            # --- // Handle Multi-Port adapters that use same serial for all interfaces \\ ---
            # Capture the dict in dup_ser it's later del if no additional devices present with the same serial
            # Capture path and ifnum for any subsequent devs if ser is already in the dup_ser dict
            _ser = devs[dev_name]['id_serial_short'] = _dev.get(
                'ID_SERIAL_SHORT', fallback_ser)
            if _ser not in devs['_dup_ser']:
                devs['_dup_ser'][_ser] = {'id_paths': [], 'id_ifnums': []}

            devs['_dup_ser'][_ser]['id_paths'].append(
                devs[dev_name]['id_path'])
            devs['_dup_ser'][_ser]['id_ifnums'].append(
                devs[dev_name]['id_ifnum'])

        # --- // Clean up detection of Multi-Port adapters that use same serial for all interfaces \\ ---
        # all dev serial #s are added to dup_ser as they are discovered
        # remove any serial #s that only appeared once.
        del_list = [
            _ser for _ser in devs['_dup_ser']
            if len(devs['_dup_ser'][_ser]['id_paths']) == 1
        ]
        for i in del_list:
            del devs['_dup_ser'][i]

        return devs if key is None else devs[key]
Esempio n. 14
0
    def menu_formatting(self,
                        section,
                        sub=None,
                        text=None,
                        footer={},
                        width=MIN_WIDTH,
                        l_offset=1,
                        index=1,
                        do_print=True,
                        do_format=True):

        mlines = []
        max_len = None
        # footer options also supports an optional formatting dict
        # place '_rjust' in the list and the subsequent item should be a dict
        #
        # _rjust: {dict} right justify addl text on same line with
        # one of the other footer options.
        # i.e.
        footer_options = {
            'power': ['p', 'Power Control Menu'],
            'dli': ['d', '[dli] Web Power Switch Menu'],
            'rshell': ['rs', 'Remote Shell Menu'],
            'key': ['k', 'Distribute SSH public Key to Remote Hosts'],
            'shell': ['sh', 'Enter Local Shell'],
            'rn': ['rn', 'Rename Adapters'],
            'refresh': ['r', 'Refresh'],
            'sync': ['s', 'Sync with cloud'],
            'con': [
                'c',
                'Change Default Serial Settings (devices marked with ** only)'
            ],
            'picohelp': ['h', 'Display Picocom Help'],
            'back': ['b', 'Back'],
            'x': ['x', 'Exit']
        }

        # -- append any errors from menu builder
        # self.error_msgs += self.menu.error_msgs
        # self.menu.error_msgs = []

        # -- Adjust width if there is an error msg longer then the current width
        # -- Delete any errors defined in ignore errors
        # TODO Move all menu formatting to it's own library - clean this up
        # Think I process errors here and maybe in print_mlines as well
        # addl processing in FOOTER

        if log.error_msgs:
            # TODO maybe move to log class
            _error_lens = []
            for _error in log.error_msgs:
                for e in self.ignored_errors:
                    _e = _error.strip('\r\n')
                    if hasattr(e, 'match') and e.match(_e):
                        log.error_msgs.remove(_error)
                        break
                    elif isinstance(e, str) and (e == _error or e in _error):
                        log.error_msgs.remove(_error)
                        break
                    else:
                        _error_lens.append(self.format_line(_error).len)
            if _error_lens:
                width = width if width >= max(_error_lens) + 5 else max(
                    _error_lens) + 5

            width = width if width <= self.cols else self.cols

        # --// HEADER \\--
        if section == 'header':
            # ---- CLEAR SCREEN -----
            if not config.debug:
                os.system('clear')
            mlines.append('=' * width)
            line = self.format_line(text)
            _len = line.len
            fmtd_header = line.text
            a = width - _len
            b = (a / 2) - 2
            if text:
                c = int(b) if b == int(b) else int(b) + 1
                if isinstance(text, list):
                    for t in text:
                        mlines.append(' {0} {1} {2}'.format(
                            '-' * int(b), t, '-' * c))
                else:
                    mlines.append(' {0} {1} {2}'.format(
                        '-' * int(b), fmtd_header, '-' * c))
            mlines.append('=' * width)

        # --// BODY \\--
        elif section == 'body':
            max_len = 0
            blines = list(text) if isinstance(text, str) else text
            pad = True if len(blines) + index > 10 else False
            indent = l_offset + 4 if pad else l_offset + 3
            width_list = []
            for _line in blines:
                # -- format spacing of item entry --
                _i = str(index) + '. ' if not pad or index > 9 else str(
                    index) + '.  '
                # -- generate line and calculate line length --
                _line = ' ' * l_offset + _i + _line
                line = self.format_line(_line)
                width_list.append(line.len)
                mlines.append(line.text)
                index += 1
            max_len = 0 if not width_list else max(width_list)
            if sub:
                # -- Add sub lines to top of menu item section --
                x = ((max_len - len(sub)) / 2) - (l_offset + (indent / 2))
                mlines.insert(0, '')
                width_list.insert(0, 0)
                if do_format:
                    mlines.insert(
                        1, '{0}{1} {2} {3}'.format(
                            ' ' * indent, '-' * int(x), sub, '-' *
                            int(x) if x == int(x) else '-' * (int(x) + 1)))
                    width_list.insert(1, len(mlines[1]))
                else:
                    mlines.insert(1, ' ' * indent + sub)
                    width_list.insert(1, len(mlines[1]))
                max_len = max(
                    width_list
                )  # update max_len in case subheading is the longest line in the section
                mlines.insert(2, ' ' * indent + '-' * (max_len - indent))
                width_list.insert(2, len(mlines[2]))

            # -- adding padding to line to full width of longest line in section --
            mlines = self.pad_lines(mlines, max_len,
                                    width_list)  # Refactoring in progress

        # --// FOOTER \\--
        elif section == 'footer':
            #######
            # Being Depricated. Remove once converted
            #######
            if text and isinstance(text, (str, list)):
                mlines.append('')
                text = [text] if isinstance(text, str) else text
                for t in text:
                    if '{{r}}' in t:
                        _t = t.split('{{r}}')
                        mlines.append('{}{}'.format(
                            _t[0], _t[1].rjust(width - len(_t[0]))))
                    else:
                        # mlines.append(self.format_line(t)[1])
                        mlines.append(self.format_line(t).text)

            # TODO temp indented this to be under text to avoid conflict during refactor
                mlines += [' x.  exit', '']
                mlines.append('=' * width)

            ########
            # REDESIGNED FOOTER LOGIC
            ########
            if footer:
                opts = utils.listify(footer.get('opts', []))
                if 'x' not in opts:
                    opts.append('x')
                no_match_overrides = no_match_rjust = []  # init
                pre_text = post_text = foot_text = []  # init
                # replace any pre-defined options with those passed in as overrides
                if footer.get('overrides') and isinstance(
                        footer['overrides'], dict):
                    footer_options = {**footer_options, **footer['overrides']}

                    no_match_overrides = [
                        e for e in footer['overrides']
                        if e not in footer_options
                        and e not in footer.get('rjust', {})
                    ]

                # update footer_options with any specially formmated (rjust) additions
                if footer.get('rjust'):
                    r = footer.get('rjust')
                    f = footer_options
                    foot_overrides = {
                        k: [
                            f[k][0], '{}{}'.format(
                                f[k][1], r[k].rjust(width - len(
                                    f' {f[k][0]}.{" " if len(f[k][0]) == 2 else "  "}{f[k][1]}'
                                )))
                        ]
                        for k in r if k in f
                    }

                    footer_options = {**footer_options, **foot_overrides}
                    no_match_rjust = [
                        e for e in footer['rjust'] if e not in footer_options
                    ]

                if footer.get('before'):
                    footer['before'] = [footer['before']] if isinstance(
                        footer['before'], str) else footer['before']
                    pre_text = [f' {line}' for line in footer['before']]

                if opts:
                    f = footer_options
                    foot_text = [
                        f' {f[k][0]}.{" " if len(f[k][0]) == 2 else "  "}{f[k][1]}'
                        for k in opts if k in f
                    ]

                if footer.get('after'):
                    footer['after'] = [footer['after']] if isinstance(
                        footer['after'], str) else footer['after']
                    post_text = [f' {line}' for line in footer['after']]

                mlines = mlines + [''] + pre_text + foot_text + post_text + [
                    ''
                ] + ['=' * width]
                # TODO probably simplify to make this a catch all at the end of this method
                # mlines = [self.format_line(line)[1] for line in mlines]
                mlines = [self.format_line(line).text for line in mlines]

                # log errors if non-match overrides/rjust options were sent
                if no_match_overrides + no_match_rjust:
                    log.error(
                        f'menu_formatting passed options ({",".join(no_match_overrides + no_match_rjust)})'
                        ' that lacked a match in footer_options = No impact to menu',
                        log=True,
                        level='error')

            # --// ERRORs - append to footer \\-- #
            if len(log.error_msgs) > 0:
                errors = log.error_msgs
                for _error in errors:
                    error = self.format_line(_error)
                    x = ((width - (error.len + 4)) / 2)
                    mlines.append('{0}{1}{2}{3}{0}'.format(
                        self.log_sym_2bang, ' ' * int(x), error.text,
                        ' ' * int(x) if x == int(x) else ' ' * (int(x) + 1)))

                if errors:  # TODO None Type added to list after rename  why
                    mlines.append('=' * width)
                if do_print:
                    log.error_msgs = []  # clear error messages after print

        else:
            log.error_msgs.append(
                'formatting function passed an invalid section')

        # --// DISPLAY THE MENU \\--
        if do_print:
            for _line in mlines:
                print(_line)
                self.menu_rows += 1  # TODO DEBUGGING make easier then remove
        # TODO refactor max_len to widest_line as thats what it is
        return mlines, max_len
Esempio n. 15
0
    def on_service_state_change(self, zeroconf: Zeroconf, service_type: str,
                                name: str,
                                state_change: ServiceStateChange) -> None:
        cpi = self.cpi
        mdns_data = None
        update_cache = False
        if state_change is ServiceStateChange.Added:
            info = zeroconf.get_service_info(service_type, name)
            if info:
                if info.server.split('.')[0] != cpi.local.hostname:
                    if info.properties:
                        properties = info.properties

                        mdns_data = {
                            k.decode('UTF-8'): v.decode('UTF-8')
                            if not v.decode('UTF-8')[0] in ['[', '{'] else
                            json.loads(v.decode('UTF-8'))  # NoQA
                            for k, v in properties.items()
                        }

                        hostname = mdns_data.get('hostname')
                        interfaces = mdns_data.get('interfaces', [])
                        # interfaces = json.loads(properties[b'interfaces'].decode("utf-8"))

                        log_out = json.dumps(mdns_data,
                                             indent=4,
                                             sort_keys=True)
                        log.debug(
                            f'[MDNS DSCVRY] {hostname} Properties Discovered via mdns:\n{log_out}'
                        )

                        rem_ip = mdns_data.get('rem_ip')
                        if not rem_ip:
                            if len(mdns_data.get('interfaces', [])) == 1:
                                rem_ip = [
                                    interfaces[i]['ip'] for i in interfaces
                                ]
                                rem_ip = rem_ip[0]
                            else:
                                rem_ip = None if hostname not in cpi.remotes.data or 'rem_ip' not in cpi.remotes.data[hostname] \
                                    else cpi.remotes.data[hostname]['rem_ip']

                        cur_known_adapters = cpi.remotes.data.get(
                            hostname, {
                                'adapters': None
                            }).get('adapters')

                        # -- Log new entry only if this is the first time it's been discovered --
                        if hostname not in self.d_discovered:
                            self.d_discovered.append(hostname)
                            log.info(
                                '[MDNS DSCVRY] {}({}) Discovered via mdns'.
                                format(hostname,
                                       rem_ip if rem_ip is not None else '?'))

                        from_mdns_adapters = mdns_data.get('adapters')
                        mdns_data['rem_ip'] = rem_ip
                        mdns_data[
                            'adapters'] = from_mdns_adapters if from_mdns_adapters is not None else cur_known_adapters
                        mdns_data['source'] = 'mdns'
                        mdns_data['upd_time'] = int(time.time())
                        mdns_data = {hostname: mdns_data}

                        # update from API only if no adapter data exists either in cache or from mdns that triggered this
                        # adapter data is updated on menu_launch
                        if not mdns_data[hostname][
                                'adapters'] or hostname not in cpi.remotes.data:
                            log.info(
                                '[MDNS DSCVRY] {} provided no adapter data Collecting via API'
                                .format(info.server.split('.')[0]))
                            # TODO check this don't think needed had a hung process on one of my Pis added it to be safe
                            try:
                                res = cpi.remotes.api_reachable(
                                    hostname, mdns_data[hostname])
                                update_cache = res.update
                                mdns_data[hostname] = res.data
                                # reachable = res.reachable
                            except Exception as e:
                                log.error(
                                    f'Exception occured verifying reachability via API for {hostname}:\n{e}'
                                )

                        if self.show:
                            if hostname in self.discovered:
                                self.discovered.remove(hostname)
                            self.discovered.append('{}{}'.format(
                                hostname, '*' if update_cache else ''))
                            print(hostname +
                                  '({}) Discovered via mdns.'.format(
                                      rem_ip if rem_ip is not None else '?'))

                            try:
                                print('{}\n{}'.format(
                                    'mdns: None' if from_mdns_adapters is None
                                    else 'mdns: {}'.format([
                                        d.replace('/dev/', '')
                                        for d in from_mdns_adapters
                                    ] if not isinstance(
                                        from_mdns_adapters, list) else [
                                            d['dev'].replace('/dev/', '')
                                            for d in from_mdns_adapters
                                        ]),
                                    'cache: None' if cur_known_adapters is None
                                    else 'cache: {}'.format([
                                        d.replace('/dev/', '')
                                        for d in cur_known_adapters
                                    ] if not isinstance(
                                        cur_known_adapters, list) else [
                                            d['dev'].replace('/dev/', '')
                                            for d in cur_known_adapters
                                        ])))
                            except TypeError as e:
                                print(f'EXCEPTION: {e}')
                            print(
                                f'\nDiscovered ConsolePis: {self.discovered}')
                            print("press Ctrl-C to exit...\n")

                        log.debug(
                            '[MDNS DSCVRY] {} Final data set:\n{}'.format(
                                hostname,
                                json.dumps(mdns_data, indent=4,
                                           sort_keys=True)))
                        if update_cache:
                            if 'hostname' in mdns_data[hostname]:
                                del mdns_data[hostname]['hostname']
                            cpi.remotes.data = cpi.remotes.update_local_cloud_file(
                                remote_consoles=mdns_data)
                            log.info(
                                f'[MDNS DSCVRY] {hostname} Local Cache Updated after mdns discovery'
                            )
                    else:
                        log.warning(
                            f'[MDNS DSCVRY] {hostname}: No properties found')
            else:
                log.warning(f'[MDNS DSCVRY] {info}: No info found')
Esempio n. 16
0
    def auto_pwron_thread(self, pwr_key):
        """Ensure any outlets linked to device are powered on

        Called by consolepi_menu exec_menu function and remote_launcher (for sessions to remotes)
        when a connection initiated with adapter.  Powers any linked outlets associated with the
        adapter on.

        params:
            menu_dev:str, The tty device user is connecting to.
        Returns:
            No Return - Updates class attributes
        """
        if self.wait_for_threads("init"):
            return

        outlets = self.pwr.data
        if "linked" not in outlets:
            _msg = "Error linked key not found in outlet dict\nUnable to perform auto power on"
            log.show(_msg, show=True)
            return

        if not outlets["linked"].get(pwr_key):
            return

        # -- // Perform Auto Power On (if not already on) \\ --
        for o in outlets["linked"][pwr_key]:
            outlet = outlets["defined"].get(o.split(":")[0])
            if outlet:
                ports = [] if ":" not in o else json.loads(
                    o.replace("'", '"').split(":")[1])
                _addr = outlet["address"]
            else:
                log.error(
                    f"Skipping Auto Power On {pwr_key} for {o}. Unable to pull outlet details from defined outlets.",
                    show=True,
                )
                log.debugv(f"Outlet Dict:\n{json.dumps(outlets)}")
                continue

            # -- // DLI web power switch Auto Power On \\ --
            #
            # TODO combine all ports from same pwr_key and sent to pwr_toggle once
            # TODO Update outlet if return is OK, then run refresh in the background to validate
            # TODO Add class attribute to cpi_menu ~ cpi_menu.new_data = "power", "main", etc
            #      Then in wait_for_input run loop to check for updates and re-display menu
            # TODO power_menu and dli_menu wait_for_threads auto power ... check cpiexec.autopwr_wait first
            #
            if outlet["type"].lower() == "dli":
                for p in ports:
                    log.debug(
                        f"[Auto PwrOn] Power ON {pwr_key} Linked Outlet {outlet['type']}:{_addr} p{p}"
                    )

                    if not outlet["is_on"][p][
                            "state"]:  # This is just checking what's in the dict not querying the DLI
                        r = self.pwr.pwr_toggle(outlet["type"],
                                                _addr,
                                                desired_state=True,
                                                port=p)
                        if isinstance(r, bool):
                            if r:
                                threading.Thread(
                                    target=self.outlet_update,
                                    kwargs={
                                        "refresh": True,
                                        "upd_linked": True
                                    },
                                    name="auto_pwr_refresh_dli",
                                ).start()
                                self.autopwr_wait = True
                        else:
                            log.warning(
                                f"{pwr_key} Error operating linked outlet @ {o}",
                                show=True,
                            )

            # -- // esphome Auto Power On \\ --
            elif outlet["type"].lower() == "esphome":
                for p in ports:
                    log.debug(
                        f"[Auto PwrOn] Power ON {pwr_key} Linked Outlet {outlet['type']}:{_addr} p{p}"
                    )
                    if not outlet["is_on"][p][
                            "state"]:  # This is just checking what's in the dict
                        r = self.pwr.pwr_toggle(outlet["type"],
                                                _addr,
                                                desired_state=True,
                                                port=p)
                        if isinstance(r, bool):
                            self.pwr.data["defined"][o.split(
                                ":")[0]]["is_on"][p]["state"] = r
                        else:
                            log.show(r)
                            log.warning(
                                f"{pwr_key} Error operating linked outlet @ {o}",
                                show=True,
                            )

            # -- // GPIO & TASMOTA Auto Power On \\ --
            else:
                log.debug(
                    f"[Auto PwrOn] Power ON {pwr_key} Linked Outlet {outlet['type']}:{_addr}"
                )
                r = self.pwr.pwr_toggle(
                    outlet["type"],
                    _addr,
                    desired_state=True,
                    noff=outlet.get("noff", True)
                    if outlet["type"].upper() == "GPIO" else True,
                )
                if isinstance(r, int) and r > 1:  # return is an error
                    r = False
                else:  # return is bool which is what we expect
                    if r:
                        self.pwr.data["defined"][o]["state"] = r
                        self.autopwr_wait = True
                        # self.pwr.pwr_get_outlets(upd_linked=True)
                    else:
                        # self.config.log_and_show(f"Error operating linked outlet {o}:{outlet['address']}", log=log.warning)
                        log.show(
                            f"Error operating linked outlet {o}:{outlet['address']}",
                            show=True,
                        )
Esempio n. 17
0
    def do_ser2net_line(self,
                        from_name: str = None,
                        to_name: str = None,
                        baud: int = None,
                        dbits: int = None,
                        parity: str = None,
                        flow: str = None,
                        sbits: int = None):
        '''Process Adapter Configuration Changes in ser2net.conf.

        Keyword Arguments:
            from_name {str} -- The Adapters existing name/alias (default: {None})
            to_name {str} -- The Adapters new name/alias (default: {None})
            baud {int} -- Adapter baud (default: {self.baud})
            dbits {int} -- Adapter databits (default: {self.data_bits})
            parity {str} -- Adapter Parity (default: {self.parity})
            flow {str} -- Adapter flow (default: {self.flow})
            sbits {int} -- Adapter stop bits (default: {self.sbits})

        Returns:
            {str|None} -- Returns error text if an error occurs or None if no issues.
        '''
        # don't add the new entry to ser2net if one already exists for the alias
        if from_name != to_name and config.ser2net_conf.get(f"/dev/{to_name}"):
            log.info(
                f"ser2net: {to_name} already mapped to port {config.ser2net_conf[f'/dev/{to_name}'].get('port')}",
                show=True)
            return

        ser2net_parity = {'n': 'NONE', 'e': 'EVEN', 'o': 'ODD'}
        ser2net_flow = {'n': '', 'x': ' XONXOFF', 'h': ' RTSCTS'}
        baud = self.baud if not baud else baud
        dbits = self.data_bits if not dbits else dbits
        parity = self.parity if not parity else parity
        flow = self.flow if not flow else flow
        sbits = self.sbits if not sbits else sbits
        log_ptr = ''

        cur_line = config.ser2net_conf.get(f'/dev/{from_name}', {}).get('line')
        if cur_line and '/dev/ttyUSB' not in cur_line and '/dev/ttyACM' not in cur_line:
            new_entry = False
            next_port = next_port = cur_line.split(':')[0]  # Renaming existing
            log_ptr = config.ser2net_conf[f'/dev/{from_name}'].get('log_ptr')
            if not log_ptr:
                log_ptr = ''
        else:
            new_entry = True
            if utils.valid_file(self.ser2net_file):
                ports = [
                    a['port'] for a in config.ser2net_conf.values()
                    if 7000 < a.get('port', 0) <= 7999
                ]
                next_port = 7001 if not ports else int(max(ports)) + 1
            else:
                next_port = 7001
                error = utils.do_shell_cmd(
                    f'sudo cp {self.ser2net_file} /etc/', handle_errors=False)
                if error:
                    log.error(
                        f'Rename Menu Error while attempting to cp ser2net.conf from src {error}'
                    )
                    return error  # error added to display in calling method

        ser2net_line = (
            '{telnet_port}:telnet:0:/dev/{alias}:{baud} {dbits}DATABITS {parity} '
            '{sbits}STOPBIT {flow} banner {log_ptr}'.format(
                telnet_port=next_port,
                alias=to_name,
                baud=baud,
                dbits=dbits,
                sbits=sbits,
                parity=ser2net_parity[parity],
                flow=ser2net_flow[flow],
                log_ptr=log_ptr))

        # -- // Append to ser2net.conf \\ --
        if new_entry:
            error = utils.append_to_file(self.ser2net_file, ser2net_line)
        # -- // Rename Existing Definition in ser2net.conf \\ --
        # -- for devices with existing definitions cur_line is the existing line
        else:
            ser2net_line = ser2net_line.strip().replace('/', r'\/')
            cur_line = cur_line.replace('/', r'\/')
            cmd = "sudo sed -i 's/^{}$/{}/'  {}".format(
                cur_line, ser2net_line, self.ser2net_file)
            error = utils.do_shell_cmd(cmd, shell=True)

        if not error:
            config.ser2net_conf = config.get_ser2net()
        else:
            return error
Esempio n. 18
0
    def on_service_state_change(self,
                                zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange) -> None:
        if self.cpi.local.hostname == name.split(".")[0]:
            return
        if state_change is not ServiceStateChange.Added:
            return

        info = zeroconf.get_service_info(service_type, name)
        if not info:
            log.warning(f'[MDNS DSCVRY] {name}: No info found')
            return
        if not hasattr(info, "properties") or not info.properties:
            log.warning(f'[MDNS DSCVRY] {name}: No properties found')
            return

        properties = info.properties

        cpi = self.cpi
        mdns_data = None
        update_cache = False
        try:
            mdns_data = {
                k.decode('UTF-8'):
                v.decode('UTF-8') if len(v) == 0 or not v.decode('UTF-8')[0] in ['[', '{'] else json.loads(v.decode('UTF-8'))  # NoQA
                for k, v in properties.items()
            }
        except Exception as e:
            log.exception(
                f"[MDNS DSCVRY] {e.__class__.__name__} occured while parsing mdns_data:\n {mdns_data}\n"
                f"Exception: \n{e}"
            )
            log.error(f"[MDNS DSCVRY] entry from {name} ignored due to parsing exception.")
            return

        hostname = mdns_data.get('hostname')
        interfaces = mdns_data.get('interfaces', [])

        log_out = json.dumps(mdns_data, indent=4, sort_keys=True)
        log.debug(f'[MDNS DSCVRY] {hostname} Properties Discovered via mdns:\n{log_out}')

        rem_ip = mdns_data.get('rem_ip')
        if not rem_ip:
            if len(mdns_data.get('interfaces', [])) == 1:
                rem_ip = [interfaces[i]['ip'] for i in interfaces]
                rem_ip = rem_ip[0]
            else:
                rem_ip = None if hostname not in cpi.remotes.data or 'rem_ip' not in cpi.remotes.data[hostname] \
                    else cpi.remotes.data[hostname]['rem_ip']

        cur_known_adapters = cpi.remotes.data.get(hostname, {'adapters': None}).get('adapters')

        # -- Log new entry only if this is the first time it's been discovered --
        if hostname not in self.d_discovered:
            self.d_discovered += [hostname]
            log.info('[MDNS DSCVRY] {}({}) Discovered via mdns'.format(
                hostname, rem_ip if rem_ip is not None else '?'))

        from_mdns_adapters = mdns_data.get('adapters')
        mdns_data['rem_ip'] = rem_ip
        mdns_data['adapters'] = from_mdns_adapters if from_mdns_adapters else cur_known_adapters
        mdns_data['source'] = 'mdns'
        mdns_data['upd_time'] = int(time.time())
        mdns_data = {hostname: mdns_data}

        # update from API only if no adapter data exists either in cache or from mdns that triggered this
        # adapter data is updated on menu_launch either way
        if (not mdns_data[hostname]['adapters'] and hostname not in self.no_adapters) or \
                hostname not in cpi.remotes.data:
            log.info(f"[MDNS DSCVRY] {info.server.split('.')[0]} provided no adapter data Collecting via API")
            # TODO check this don't think needed had a hung process on one of my Pis added it to be safe
            try:
                # TODO we are setting update time here so always result in a cache update with the restart timer
                res = cpi.remotes.api_reachable(hostname, mdns_data[hostname])
                update_cache = res.update
                if not res.data.get('adapters'):
                    self.no_adapters.append(hostname)
                elif hostname in self.no_adapters:
                    self.no_adapters.remove(hostname)
                mdns_data[hostname] = res.data
            except Exception as e:
                log.exception(f'Exception occurred verifying reachability via API for {hostname}:\n{e}')

        if self.show:
            if hostname in self.discovered:
                self.discovered.remove(hostname)
            self.discovered.append('{}{}'.format(hostname, '*' if update_cache else ''))
            print(hostname + '({}) Discovered via mdns.'.format(rem_ip if rem_ip is not None else '?'))

            try:
                print(
                    '{}\n{}\n{}'.format(
                        'mdns: None' if from_mdns_adapters is None else 'mdns: {}'.format(
                            [d.replace('/dev/', '') for d in from_mdns_adapters]
                            if not isinstance(from_mdns_adapters, list) else
                            [d['dev'].replace('/dev/', '') for d in from_mdns_adapters]
                        ),
                        'api (mdns trigger): None' if not mdns_data[hostname]['adapters'] else 'api (mdns trigger): {}'.format(
                            [d.replace('/dev/', '') for d in mdns_data[hostname]['adapters']]
                            if not isinstance(mdns_data[hostname]['adapters'], list) else
                            [d['dev'].replace('/dev/', '') for d in mdns_data[hostname]['adapters']]
                        ),
                        'cache: None' if cur_known_adapters is None else 'cache: {}'.format(
                            [d.replace('/dev/', '') for d in cur_known_adapters]
                            if not isinstance(cur_known_adapters, list) else
                            [d['dev'].replace('/dev/', '') for d in cur_known_adapters]
                        )
                    )
                )
            except TypeError as e:
                print(f'EXCEPTION: {e}')

            print(f'\nDiscovered ConsolePis: {self.discovered}')
            print("press Ctrl-C to exit...\n")

        log.debugv(
            f"[MDNS DSCVRY] {hostname} Final data set:\n{json.dumps(mdns_data, indent=4, sort_keys=True)}"
        )

        # TODO could probably just put the call to cache update in the api_reachable method
        if update_cache:
            if 'hostname' in mdns_data[hostname]:
                del mdns_data[hostname]['hostname']
            cpi.remotes.data = cpi.remotes.update_local_cloud_file(remote_consoles=mdns_data)
            log.info(f'[MDNS DSCVRY] {hostname} Local Cache Updated after mdns discovery')