Example #1
0
    def gen_copy_key(self, rem_data=None):
        """Generate public ssh key and distribute to remote ConsolePis

        Keyword Arguments:
            rem_data {tuple or list of tuples} -- each tuple should have 3 items
            0: hostname of remote, 1: rem_ip, 3: rem_user    (default: {None})

        Returns:
            {list} -- list of any errors reported, could be informational
        """
        hostname = self.local.hostname
        loc_user = self.local.user
        loc_home = self.local.loc_home

        # -- generate local key file if it doesn't exist
        if not os.path.isfile(loc_home + "/.ssh/id_rsa"):
            print("\nNo Local ssh cert found, generating...\n")
            utils.do_shell_cmd(
                f'sudo -u {loc_user} ssh-keygen -m pem -t rsa -C "{loc_user}@{hostname}"', timeout=360
            )

        # -- copy keys to remote(s)
        if not isinstance(rem_data, list):
            rem_data = [rem_data]
        return_list = []
        for _rem in rem_data:
            rem, rem_ip, rem_user = _rem
            print(self.menu.format_line("{{magenta}}Attempting to copy ssh cert to " + rem + "{{norm}}").text)
            ret = utils.do_shell_cmd(
                f"sudo -u {loc_user} ssh-copy-id {rem_user}@{rem_ip}", timeout=360
            )
            if ret is not None:
                return_list.append("{}: {}".format(rem, ret))
        return return_list
Example #2
0
 def get_cpu_serial(self):
     res = utils.do_shell_cmd(
         "/bin/cat /proc/cpuinfo | grep Serial | awk '{print $3}'",
         return_stdout=True)
     if res[0] > 0:
         log.warning(
             'Unable to get unique identifier for this pi (cpuserial)',
             show=True)
     else:
         return res[1] or '0'
Example #3
0
    def menu_exec(self, choice, menu_actions, calling_menu="main_menu"):
        '''Execute Menu Selection.  This method needs to be overhauled.

        The ConsolePiAction object defined but not used in __init__ is part of the plan for overhaul
        menu will build insance of the object for each selection. That will be used to determine what
        action to perform and what to do after etc.
        '''
        pwr = self.pwr

        if not config.debug and calling_menu not in ["dli_menu", "power_menu"]:
            os.system("clear")

        if (
            not choice.lower
            or choice.lower in menu_actions
            and menu_actions[choice.lower] is None
        ):
            (
                self.menu.rows,
                self.menu.cols,
            ) = (
                utils.get_tty_size()
            )  # re-calc tty size in case they've adjusted the window
            return

        else:
            ch = choice.lower
            try:  # Invalid Selection
                if isinstance(menu_actions[ch], dict):
                    if menu_actions[ch].get("cmd"):
                        # TimeStamp for picocom session log file if defined
                        menu_actions[ch]["cmd"] = menu_actions[ch]["cmd"].replace(
                            "{{timestamp}}", time.strftime("%F_%H.%M")
                        )

                        # -- // AUTO POWER ON LINKED OUTLETS \\ --
                        if (
                            config.power and "pwr_key" in menu_actions[ch]
                        ):
                            self.exec_auto_pwron(menu_actions[ch]["pwr_key"])

                        # -- // Print pre-connect messsge if provided \\ --
                        if menu_actions[ch].get("pre_msg"):
                            print(menu_actions[ch]["pre_msg"])

                        # --// execute the command \\--
                        try:
                            _error = None
                            if "exec_kwargs" in menu_actions[ch]:
                                c = menu_actions[ch]["cmd"]
                                _error = utils.do_shell_cmd(
                                    c, **menu_actions[ch]["exec_kwargs"]
                                )
                                if _error and self.autopwr_wait:
                                    # TODO simplify this after moving to action object
                                    _method = "ssh -t" if "ssh" in c else "telnet"
                                    if "ssh" in _method:
                                        _h = (
                                            c.split(f"{_method} ")[1]
                                            .split(" -p")[0]
                                            .split("@")[1]
                                        )
                                        _p = int(c.split("-p ")[1])
                                    elif _method == "telnet":
                                        c_list = c.split()
                                        _h = c_list[-2]
                                        _p = int(c_list[-1])

                                    def wait_for_boot():
                                        while True:
                                            try:
                                                if utils.is_reachable(_h, _p, silent=True):
                                                    break
                                                else:
                                                    time.sleep(3)
                                            except KeyboardInterrupt:
                                                self.autopwr_wait = False
                                                log.show("Connection Aborted")
                                                break

                                    print(
                                        "\nInitial Attempt Failed, but host is linked to an outlet that was"
                                    )
                                    print("off. Host may still be booting\n")
                                    utils.spinner(
                                        f"Waiting for {_h} to boot, CTRL-C to Abort",
                                        wait_for_boot,
                                    )
                                    if self.autopwr_wait:
                                        _error = utils.do_shell_cmd(c, **menu_actions[ch]["exec_kwargs"])
                                        self.autopwr_wait = False
                            else:
                                c = shlex.split(menu_actions[ch]["cmd"])
                                result = subprocess.run(c, stderr=subprocess.PIPE)
                                _stderr = result.stderr.decode("UTF-8")
                                if _stderr or result.returncode == 1:
                                    _error = utils.error_handler(c, _stderr)

                            if _error:
                                log.show(_error)

                            # -- // resize the terminal to handle serial connections that jack the terminal size \\ --
                            c = " ".join([str(i) for i in c])
                            if "picocom" in c:
                                os.system(
                                    "/etc/ConsolePi/src/consolepi-commands/resize >/dev/null"
                                )

                        except KeyboardInterrupt:
                            log.show("Aborted last command based on user input")

                    elif "function" in menu_actions[ch]:
                        args = (
                            menu_actions[ch]["args"]
                            if "args" in menu_actions[ch]
                            else []
                        )
                        kwargs = (
                            menu_actions[ch]["kwargs"]
                            if "kwargs" in menu_actions[ch]
                            else {}
                        )
                        confirmed, spin_text, name = self.confirm_and_spin(
                            menu_actions[ch], *args, **kwargs
                        )
                        if confirmed:
                            # update kwargs with name from confirm_and_spin method
                            if menu_actions[ch]["function"].__name__ == "pwr_rename":
                                kwargs["name"] = name

                            # // -- CALL THE FUNCTION \\--
                            if (
                                spin_text
                            ):  # start spinner if spin_text set by confirm_and_spin
                                with Halo(text=spin_text, spinner="dots2"):
                                    response = menu_actions[ch]["function"](
                                        *args, **kwargs
                                    )
                            else:  # no spinner
                                response = menu_actions[ch]["function"](*args, **kwargs)

                            # --// Power Menus \\--
                            if calling_menu in ["power_menu", "dli_menu"]:
                                if menu_actions[ch]["function"].__name__ == "pwr_all":
                                    with Halo(
                                        text="Refreshing Outlet States", spinner="dots"
                                    ):
                                        self.outlet_update(
                                            refresh=True, upd_linked=True
                                        )  # TODO can I move this to Outlets Class
                                else:
                                    _grp = menu_actions[ch]["key"]
                                    _type = menu_actions[ch]["args"][0]
                                    _addr = menu_actions[ch]["args"][1]
                                    # --// EVAL responses for dli outlets \\--
                                    if _type == "dli":
                                        host_short = utils.get_host_short(_addr)
                                        _port = menu_actions[ch]["kwargs"]["port"]
                                        # --// Operations performed on ALL outlets \\--
                                        if (
                                            isinstance(response, bool)
                                            and _port is not None
                                        ):
                                            if (
                                                menu_actions[ch]["function"].__name__
                                                == "pwr_toggle"
                                            ):
                                                self.spin.start(
                                                    "Request Sent, Refreshing Outlet States"
                                                )
                                                # threading.Thread(target=self.get_dli_outlets,
                                                # kwargs={'upd_linked': True, 'refresh': True}, name='pwr_toggle_refresh').start()
                                                upd_linked = (
                                                    True
                                                    if calling_menu == "power_menu"
                                                    else False
                                                )  # else dli_menu
                                                threading.Thread(
                                                    target=self.outlet_update,
                                                    kwargs={
                                                        "upd_linked": upd_linked,
                                                        "refresh": True,
                                                    },
                                                    name="pwr_toggle_refresh",
                                                ).start()
                                                if _grp in pwr.data["defined"]:
                                                    pwr.data["defined"][_grp]["is_on"][
                                                        _port
                                                    ]["state"] = response
                                                elif _port != "all":
                                                    pwr.data["dli_power"][_addr][_port][
                                                        "state"
                                                    ] = response
                                                else:  # dli toggle all
                                                    for t in threading.enumerate():
                                                        if (
                                                            t.name
                                                            == "pwr_toggle_refresh"
                                                        ):
                                                            t.join()  # if refresh thread is running join ~
                                                            # wait for it to complete.
                                                            # TODO Don't think this works or below
                                                            # wouldn't have been necessary.

                                                            # toggle all returns True (ON) or False (OFF) if command
                                                            # successfully sent.  In reality the ports
                                                            # may not be in the  state yet, but dli is working it.
                                                            # Update menu items to reflect end state
                                                            for p in pwr.data[
                                                                "dli_power"
                                                            ][_addr]:
                                                                pwr.data["dli_power"][
                                                                    _addr
                                                                ][p]["state"] = response
                                                            break
                                                self.spin.stop()
                                            # Cycle operation returns False if outlet is off, only valid on powered outlets
                                            elif (
                                                menu_actions[ch]["function"].__name__
                                                == "pwr_cycle"
                                                and not response
                                            ):
                                                log.show(
                                                    f"{host_short} Port {_port} is Off.  Cycle is not valid"
                                                )
                                            elif (
                                                menu_actions[ch]["function"].__name__
                                                == "pwr_rename"
                                            ):
                                                if response:
                                                    _name = pwr._dli[_addr].name(_port)
                                                    if _grp in pwr.data.get(
                                                        "defined", {}
                                                    ):
                                                        pwr.data["defined"][_grp][
                                                            "is_on"
                                                        ][_port]["name"] = _name
                                                    else:
                                                        threading.Thread(
                                                            target=self.outlet_update,
                                                            kwargs={
                                                                "upd_linked": True,
                                                                "refresh": True,
                                                            },
                                                            name="pwr_rename_refresh",
                                                        ).start()
                                                    pwr.data["dli_power"][_addr][_port][
                                                        "name"
                                                    ] = _name
                                        # --// str responses are errors append to error_msgs \\--
                                        # TODO refactor response to use new cpi.response(...)
                                        elif (
                                            isinstance(response, str)
                                            and _port is not None
                                        ):
                                            log.show(response)
                                        # --// Can Remove After Refactoring all responses to bool or str \\--
                                        elif isinstance(response, int):
                                            if (
                                                menu_actions[ch]["function"].__name__
                                                == "pwr_cycle"
                                                and _port == "all"
                                            ):
                                                if response != 200:
                                                    log.show(
                                                        "Error Response Returned {}".format(
                                                            response
                                                        )
                                                    )
                                            # This is a catch as for the most part I've tried to refactor so the pwr library
                                            # returns port state on success (True/False)
                                            else:
                                                if response in [200, 204]:
                                                    log.show(
                                                        "DEV NOTE: check pwr library ret=200 or 204"
                                                    )
                                                else:
                                                    _action = menu_actions[ch][
                                                        "function"
                                                    ].__name__
                                                    log.show(
                                                        f"Error returned from dli {host_short} when "
                                                        f"attempting to {_action} port {_port}"
                                                    )
                                    # --// EVAL responses for espHome outlets \\--
                                    elif _type == "esphome":
                                        host_short = utils.get_host_short(_addr)
                                        _port = menu_actions[ch]["kwargs"]["port"]
                                        # --// Operations performed on ALL outlets \\--
                                        if (isinstance(response, bool) and _port is not None):
                                            pwr.data['defined'][_grp]['is_on'][_port]['state'] = response
                                            if (
                                                menu_actions[ch]["function"].__name__
                                                == "pwr_cycle"
                                                and not response
                                            ):
                                                _msg = f"{_grp}({host_short})" if _grp != host_short else f"{_grp}"
                                                if _msg != _port:
                                                    _msg = f"{_msg} Port {_port} is Off. Cycle is not valid"
                                                else:
                                                    _msg = f"{_msg} is Off. Cycle is not valid"
                                                log.show(_msg)
                                        elif (isinstance(response, str) and _port is not None):
                                            log.show(response)

                                    # --// EVAL responses for GPIO and tasmota outlets \\--
                                    else:
                                        if (
                                            menu_actions[ch]["function"].__name__
                                            == "pwr_toggle"
                                        ):
                                            if _grp in pwr.data.get("defined", {}):
                                                if isinstance(response, bool):
                                                    pwr.data["defined"][_grp][
                                                        "is_on"
                                                    ] = response
                                                else:
                                                    pwr.data["defined"][_grp][
                                                        "errors"
                                                    ] = response
                                        elif (
                                            menu_actions[ch]["function"].__name__
                                            == "pwr_cycle"
                                            and not response
                                        ):
                                            log.show(
                                                "Cycle is not valid for Outlets in the off state"
                                            )
                                        elif (
                                            menu_actions[ch]["function"].__name__
                                            == "pwr_rename"
                                        ):
                                            log.show(
                                                "rename not yet implemented for {} outlets".format(
                                                    _type
                                                )
                                            )
                            elif calling_menu in ["key_menu", "rename_menu"]:
                                if response:
                                    log.show(response)
                        else:  # not confirmed
                            log.show("Operation Aborted by User")
                elif menu_actions[ch].__name__ in ["power_menu", "dli_menu"]:
                    menu_actions[ch](calling_menu=calling_menu)
                else:
                    menu_actions[ch]()
            except KeyError as e:
                if len(choice.orig) <= 2 or not self.exec_shell_cmd(choice.orig):
                    log.show(f"Invalid selection {e}, please try again.")
        return True
Example #4
0
    def add_to_udev(self,
                    udev_line: str,
                    section_marker: str,
                    label: str = None):
        '''Add or edit udev rules file with new symlink after adapter rename.

        Arguments:
            udev_line {str} -- The properly formatted udev line being added to the file
            section_marker {str} -- Match text used to determine where to place the line

        Keyword Arguments:
            label {str} -- The rules file GOTO label used in some scenarios
                           (i.e. multi-port 1 serial) (default: {None})

        Returns:
            {str|None} -- Returns error string if an error occurs
        '''
        found = ser_label_exists = get_next = update_file = False  # init
        goto = line = cmd = ''  # init
        rules_file = self.rules_file  # if 'ttyAMA' not in udev_line else self.ttyama_rules_file  Testing 1 rules file
        if utils.valid_file(rules_file):
            with open(rules_file) as x:
                for line in x:
                    # temporary for those that have the original file
                    if 'ID_SERIAL' in line and 'IMPORT' not in line:
                        _old = 'ENV{ID_SERIAL}=="", GOTO="BYPATH-POINTERS"'
                        _new = 'ENV{ID_SERIAL_SHORT}=="", IMPORT{builtin}="path_id", GOTO="BYPATH-POINTERS"'
                        cmd = "sudo sed -i 's/{}/{}/' {}".format(
                            _old, _new, rules_file)
                        update_file = True

                    # No longer including SUBSYSTEM in formatted udev line, redundant given logic @ top of rules file
                    if line.replace('SUBSYSTEM=="tty", ',
                                    '').strip() == udev_line.strip():
                        return  # Line is already in file Nothing to do.
                    if get_next:
                        goto = line
                        get_next = False
                    if section_marker.replace(' END', '') in line:
                        get_next = True
                    elif section_marker in line:
                        found = True
                    elif label and 'LABEL="{}"'.format(label) in line:
                        ser_label_exists = True

                last_line = line
            if update_file:
                error = utils.do_shell_cmd(cmd)
                if error:
                    log.show(error)

            goto = goto.split('GOTO=')[1].replace(
                '"', '').strip() if 'GOTO=' in goto else None
            if goto is None:
                goto = last_line.strip().replace('LABEL=', '').replace(
                    '"', '') if 'LABEL=' in last_line else None
        else:
            error = utils.do_shell_cmd(
                f'sudo cp /etc/ConsolePi/src/{os.path.basename(rules_file)} /etc/udev/rules.d/'
            )
            # TODO switch to pathlib.Path('path...').copy(src, dst)
            found = True
            goto = 'END'

        if goto and 'GOTO=' not in udev_line:
            udev_line = '{}, GOTO="{}"'.format(udev_line, goto)

        if label and not ser_label_exists:
            udev_line = 'LABEL="{}"\\n{}'.format(label, udev_line)

        # -- // UPDATE RULES FILE WITH FORMATTED LINE \\ --
        if found:
            udev_line = '{}\\n{}'.format(udev_line, section_marker)
            cmd = "sudo sed -i 's/{}/{}/' {}".format(section_marker, udev_line,
                                                     rules_file)
            error = utils.do_shell_cmd(cmd, handle_errors=False)
            if error:
                return error
        else:  # Not Using new 10-ConsolePi.rules template just append to file
            if section_marker == '# END BYSERIAL-DEVS':
                return utils.append_to_file(rules_file, udev_line)
            else:  # if not by serial device the new template is required
                return 'Unable to Add Line, please use the new 10.ConsolePi.rules found in src dir and\n' \
                    'add you\'re current rules to the BYSERIAL-DEVS section.'
Example #5
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
Example #6
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'
Example #7
0
    def get_ser2net(self):
        '''Parse ser2net.conf to extract connection info for serial adapters

        retruns 2 level dict (empty dict if ser2net.conf not found or empty):
            {
                <adapter name or alias>: {
                    "baud": <baud>,
                    "dbits": <data bits>,
                    "flow": "<flow control>",
                    "parity": "<parity>",
                    "sbits": <stop bits>,
                    "port": <telnet port (ser2net),
                    "logfile": None or logfile if defined in ser2net.conf
                    "cmd": picocom command string used in menu
                    "line": The line from ser2net.conf
                }
            }
        '''
        ########################################################
        # --- ser2net (3.x) config lines look like this ---
        # ... 9600 NONE 1STOPBIT 8DATABITS XONXOFF LOCAL -RTSCTS
        # ... 9600 8DATABITS NONE 1STOPBIT banner
        ########################################################
        # utils = self.utils
        if not utils.valid_file(self.static.get('SER2NET_FILE')):
            log.warning(
                'No ser2net.conf file found unable to extract port definition',
                show=True)
            return {}

        ser2net_conf = {}
        trace_files = {}
        with open(self.static['SER2NET_FILE']) as cfg:
            for line in cfg:
                if 'TRACEFILE:' in line:
                    line = line.split(':')
                    trace_files[line[1]] = line[2]
                    continue
                elif not line[0].isdigit():
                    continue
                _line = line.strip('\n')
                line = line.split(':')
                tty_port = int(line[0])
                tty_dev = line[3]

                # Reset defaults
                # baud is used to determine parsing failure
                dbits = 8
                parity = 'n'
                flow = 'n'
                sbits = 1
                logfile = None
                log_ptr = None

                connect_params = line[4].replace(',', ' ').split()
                baud = None
                for option in connect_params:
                    if option in self.static.get('VALID_BAUD', [
                            '300', '1200', '2400', '4800', '9600', '19200',
                            '38400', '57600', '115200'
                    ]):
                        baud = int(option)
                    elif 'DATABITS' in option:
                        dbits = int(option.replace('DATABITS',
                                                   ''))  # int 5 - 8
                        if dbits < 5 or dbits > 8:
                            log.warning(
                                f'{tty_dev}: Invalid value for "data bits" found in ser2net.conf falling back to 8',
                                show=True)
                            dbits = 8
                    elif option in ['EVEN', 'ODD', 'NONE']:
                        parity = option[0].lower(
                        )  # converts to e o n used by picocom
                    elif option == 'XONXOFF':
                        flow = 'x'
                    elif option == 'RTSCTS':
                        flow = 'h'
                    elif 'STOPBIT' in option:  # Not used by picocom
                        sbits = int(option[0]) if option[0].isdigit else 1
                    elif 'tb=' in option or 'tr=' in option or 'tw=' in option:
                        log_ptr = option
                        logfile = option.split('=')[1]

                # Use baud to determine if options were parsed correctly
                if baud is None:
                    log.warning(
                        f'{tty_dev} found in ser2net but unable to parse baud falling back to {self.default_baud}',
                        show=True)
                    baud = self.default_baud

                # parse TRACEFILE defined in ser2net.conf
                cmd_base = f'picocom {tty_dev} --baud {baud} --flow {flow} --databits {dbits} --parity {parity}'
                if self.picocom_ver > 1:  # picocom ver 1.x in Stretch doesn't support "--stopbits"
                    cmd_base = cmd_base + f' --stopbits {sbits}'
                if logfile:
                    logfile = trace_files[logfile]
                    logfile = logfile.replace('\\p', str(tty_port)).replace(
                        '\\d',
                        tty_dev.split('/')[-1])
                    logfile = logfile.replace(
                        '\\s', f'{baud}_{dbits}{parity.upper()}{sbits}')
                    logfile = logfile.split(
                        '\\'
                    )[0] + '-{{timestamp}}.log'  # + time.strftime('%H.%M.log')
                    cmd = cmd_base + f' --logfile {logfile}'
                    utils.do_shell_cmd(
                        f"mkdir -p {'/'.join(logfile.split('/')[0:-1])}")
                    utils.set_perm('/'.join(logfile.split('/')[0:-1]))
                else:
                    cmd = cmd_base

                # update dict with values for this device
                ser2net_conf[tty_dev] = {
                    'port': tty_port,
                    'baud': baud,
                    'dbits': dbits,
                    'parity': parity,
                    'flow': flow,
                    'sbits': sbits,
                    'logfile': logfile,
                    'log_ptr': log_ptr,
                    'cmd': cmd,
                    'line': _line
                }

        return ser2net_conf
Example #8
0
if add_del != "tftp":
    log.info(
        f'[DHCP LEASE] DHCP Client Connected ({add_del}): iface: {iface}, mac: {mac_bytes}, ip: {ip}, vendor: {vendor}'
    )
    ztp = False
else:
    ztp = True
    file_size = os.stat(cfg_file).st_size
    ztp_ok = True if int(mac_bytes) == file_size else False
    mac = utils.Mac(get_mac(ip))
    log.info(
        f"[ZTP - TFTP XFR] {os.path.basename(cfg_file)} sent to {ip}|{mac.cols}{' Success' if ztp_ok else ''}"
    )
    _res = utils.do_shell_cmd(
        f"wall 'consolepi-ztp: {os.path.basename(cfg_file)} sent to "
        f"{ip}|{mac.cols}{' Success' if ztp_ok else ' WARNING xfr != file size'}'"
    )
    if not ztp_ok:
        log.warning(
            f"File Size {file_size} and Xfr Total ({mac_bytes}) don't match")
    next_ztp(cfg_file, mac)

    # -- Some old log only stuff, may use for post deployment actions --
    if vendor and 'ConsolePi' in vendor:
        log.info(f'A ConsolePi has connected to {iface}')
    elif vendor and iface and 'eth' in iface:
        for _ in match:
            if _ in vendor:
                if utils.is_reachable(ip, 22):
                    log.info('{} is reachable via ssh @ {}'.format(_, ip))
                elif utils.is_reachable(ip, 23):