Ejemplo n.º 1
0
    def exec_shell_cmd(self, cmd):
        """Determine if cmd is valid shell cmd and execute if so.

        Command will execute as the local user unless the user prefixed the
        cmd with sudo -u

        Arguments:
            cmd {str} -- Input provided by user

        Returns:
            True|None -- Return True if cmd was determined to be a bash cmd
        """
        s = subprocess
        c = cmd.replace("-u", "").replace("sudo", "").strip()
        p = "PATH=$PATH:/etc/ConsolePi/src/consolepi-commands && "
        # TODO if shutil.which(c.split()[0]):
        r = s.run(f"{p}which {c.split()[0]}",
                  shell=True,
                  stderr=s.PIPE,
                  stdout=s.PIPE)
        if r.returncode == 0:
            try:
                if "sudo " not in cmd:
                    cmd = f'sudo -u {config.loc_user} bash -c "{p}{cmd}"'
                elif "sudo -u " not in cmd:
                    cmd = cmd.replace("sudo ", "")
                subprocess.run(cmd, shell=True)
                print("")
                input("Press Enter to Continue... ")
            except (KeyboardInterrupt, EOFError):
                log.show('Operation Aborted')
                print('')  # prevents header and prompt on same line in debug

            return True
Ejemplo n.º 2
0
 def no_creds_error(self):
     cloud_svc = config.cfg.get("cloud_svc", "UNDEFINED!")
     log.warning(
         f"Required {cloud_svc} credentials files are missing refer to GitHub for details"
     )
     log.warning(f"Disabling {cloud_svc} updates")
     log.show("Cloud Function Disabled by script - No Credentials Found")
     self.do_cloud = config.cfg["do_cloud"] = False
Ejemplo n.º 3
0
    def get_outlets_from_file(self):
        '''Get outlets defined in config

        returns:
            dict: with following keys (all values are dicts)
                linked: linked outlets from config (linked to serial adapters- auto pwr-on)
                dli_power: dict any dlis in config have all ports represented here
                failures: failure to connect to any outlets will result in an entry here
                    outlet_name: failure description
        '''
        outlet_data = self.cfg_yml.get('POWER')
        if not outlet_data:  # fallback to legacy json config
            outlet_data = self.get_json_file(self.static.get('POWER_FILE'))

        if not outlet_data:
            if self.power:
                log.show('Power Function Disabled - Configuration Not Found')
                self.power = False
            self.outlet_types = []
            return outlet_data

        types = []
        by_dev = {}
        for k in outlet_data:
            if outlet_data[k].get('linked_devs'):
                outlet_data[k]['linked_devs'] = utils.format_dev(outlet_data[k]['linked_devs'],
                                                                 hosts=self.hosts, with_path=True)
                self.linked_exists = True
                for dev in outlet_data[k]['linked_devs']:
                    _type = outlet_data[k].get('type').lower()
                    if _type == 'dli':
                        _this = [f"{k}:{[int(p) for p in utils.listify(outlet_data[k]['linked_devs'][dev])]}"]
                    elif _type == 'esphome':
                        _linked = utils.listify(outlet_data[k]['linked_devs'][dev])
                        _this = [f'{k}:{[p for p in _linked]}']
                    else:
                        _this = [k]
                    by_dev[dev] = _this if dev not in by_dev else by_dev[dev] + _this
            else:
                outlet_data[k]['linked_devs'] = []

            if outlet_data[k]["type"].lower() not in types:
                types.append(outlet_data[k]["type"].lower())

            if outlet_data[k]['type'].upper() == 'GPIO' and isinstance(outlet_data[k].get('address'), str) \
                    and outlet_data[k]['address'].isdigit():
                outlet_data[k]['address'] = int(outlet_data[k]['address'])

        self.outlet_types = types
        outlet_data = {
            'defined': outlet_data,
            'linked': by_dev,
            'dli_power': {},
            'failures': {}
            }

        return outlet_data
Ejemplo n.º 4
0
 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
Ejemplo n.º 5
0
    def trigger_udev(self):
        '''reload/trigger udev (udevadm)

        Returns:
            No return unless there is an error.
            Returns {str} if an error occurs.
        '''
        cmd = 'sudo udevadm control --reload && sudo udevadm trigger && systemctl is-active ser2net >/dev/null 2>&1'\
              '&& sudo systemctl stop ser2net && sleep 1 && sudo systemctl start ser2net'
        error = utils.spinner(
            'Triggering reload of udev & ser2net due to name change',
            utils.do_shell_cmd,
            cmd,
            shell=True)
        if not error:
            self.udev_pending = False
        else:
            log.show(
                'Failed to reload udev rules, you may need to rectify manually for adapter names to display correctly'
            )
            log.show(
                f'Check /var/log/syslog for errors, the rules file ({self.rules_file}) and reattempt {cmd} manually'
            )
            log.show(error)
Ejemplo n.º 6
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])
            ports = [] if ":" not in o else json.loads(o.split(":")[1])  # NoQA .replace('\'', '"')) No longer necessary single port defs are listified in config.py
            _addr = outlet["address"]

            # -- // DLI web power switch Auto Power On \\ --
            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}"
                    )
                    # TODO have seen this, but unable to recreate.  may be transient failure???
                    # NoQA This log occurs: [ERROR]: [DLI GET OUTLETS] dli @ labpower2.kabrew.com reachable, but failed to fetch statuslist (outlet_list)
                    # is_on in pwr.data['labpower2']['defined'] is being flushed based on error above so empty dict resulting in key error
                    # Exception in thread auto_pwr_on_r1-8320T-sw:
                    # Traceback (most recent call last):
                    # File "/usr/lib/python3.7/threading.py", line 917, in _bootstrap_inner
                    #     self.run()
                    # File "/usr/lib/python3.7/threading.py", line 865, in run
                    #     self._target(*self._args, **self._kwargs)
                    # File "/etc/ConsolePi/src/pypkg/consolepi/exec.py", line 88, in auto_pwron_thread
                    #     if not outlet["is_on"][p][
                    # KeyError: 2
                    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,
                        )
Ejemplo n.º 7
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
Ejemplo n.º 8
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])
            ports = [] if ":" not in o else json.loads(
                o.replace("'", '"').split(":")[1])
            _addr = outlet["address"]

            # -- // 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,
                        )
Ejemplo n.º 9
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
Ejemplo n.º 10
0
    def api_reachable(self,
                      remote_host: str,
                      cache_data: dict,
                      rename: bool = False):
        """Check Rechability & Fetch adapter data via API for remote ConsolePi

        params:
            remote_host:str, The hostname of the Remote ConsolePi
            cache_data:dict, The ConsolePi dictionary for the remote (from cache file)
            rename:bool, rename = True will do api call with refresh=True Query parameter
                which tells the api to first update connection data from ser2net as it likely
                changed as a result of remote rename operation.

        returns:
            tuple [0]: Bool, indicating if data is different than cache
                  [1]: dict, Updated ConsolePi dictionary for the remote
        """
        class ApiReachableResponse:
            def __init__(self, update, data, reachable):
                self.update = update
                self.data = data
                self.reachable = reachable

        update = False
        local = self.local

        _iface_dict = cache_data["interfaces"]
        rem_ip_list = [
            _iface_dict[_iface].get("ip") for _iface in _iface_dict
            if not _iface.startswith("_")
            and _iface_dict[_iface].get("ip") not in local.ip_list
        ]

        # if inbound data includes rem_ip make sure to try that first
        for _ip in [cache_data.get("rem_ip"), cache_data.get("last_ip")]:
            if _ip:
                if _ip not in rem_ip_list or rem_ip_list.index(_ip) != 0:
                    rem_ip_list.remove(_ip)
                    rem_ip_list.insert(0, _ip)

        rem_ip = None
        for _ip in rem_ip_list:
            log.debug(f"[API_REACHABLE] verifying {remote_host}")
            _adapters = self.get_adapters_via_api(_ip, rename=rename)
            if _adapters:
                rem_ip = _ip  # Remote is reachable
                if not isinstance(
                        _adapters,
                        int):  # indicates an html error code was returned
                    if isinstance(
                            _adapters, list
                    ):  # indicates need for conversion from old api format
                        _adapters = self.convert_adapters(_adapters)
                        if not self.old_api_log_sent:
                            log.warning(
                                f"{remote_host} provided old api schema.  Recommend Upgrading to current."
                            )
                            self.old_api_log_sent = True
                    # Only compare config dict for each adapter as udev dict will generally be different due to time_since_init
                    if not cache_data.get("adapters") or {
                            a: {
                                "config": _adapters[a].get("config", {})
                            }
                            for a in _adapters
                    } != {
                            a: {
                                "config": cache_data["adapters"][a].get(
                                    "config", {})
                            }
                            for a in cache_data["adapters"]
                    }:
                        cache_data["adapters"] = _adapters
                        update = True  # --> Update if adapter dict is different
                    else:
                        cached_udev = [
                            False for a in cache_data["adapters"]
                            if 'udev' not in cache_data["adapters"][a]
                        ]
                        if False in cached_udev:
                            cache_data["adapters"] = _adapters
                            update = True  # --> Update if udev key not in existing data (udev not sent to cloud)
                elif _adapters == 200:
                    log.show(
                        f"Remote {remote_host} is reachable via {_ip},"
                        " but has no adapters attached\nit's still available in remote shell menu"
                    )

                # remote was reachable update last_ip, even if returned bad status_code still reachable
                if not cache_data.get("last_ip", "") == _ip:
                    cache_data["last_ip"] = _ip
                    update = True  # --> Update if last_ip is different than currently reachable IP
                break

        if cache_data.get("rem_ip") != rem_ip:
            cache_data["rem_ip"] = rem_ip
            update = (
                True  # --> Update if rem_ip didn't match (was previously unreachable)
            )

        if not _adapters:
            reachable = False
            if isinstance(cache_data.get("adapters"), list):
                _adapters = cache_data.get("adapters")
                _adapters = {
                    _adapters[_adapters.index(d)]["dev"]: {
                        "config": {
                            k: _adapters[_adapters.index(d)][k]
                            for k in _adapters[_adapters.index(d)]
                        }
                    }
                    for d in _adapters
                }
                cache_data["adapters"] = _adapters
                _msg = (
                    f"{remote_host} Cached adapter data was in old format... Converted to new.\n"
                    f"\t\t{remote_host} Should be upgraded to the current version of ConsolePi."
                )
                log.warning(_msg, show=True)
                update = True  # --> Convert to new and Update if cache data was in old format
        else:
            reachable = True

        return ApiReachableResponse(update, cache_data, reachable)
Ejemplo n.º 11
0
    def refresh(self, bypass_cloud=False):
        remote_consoles = None
        cpiexec = self.cpiexec
        local = self.local
        cloud_svc = config.cfg.get("cloud_svc", "error")

        # TODO refactor wait_for_threads to have an all key or accept a list
        with Halo(text="Waiting For threads to complete", spinner="dots1"):
            if cpiexec.wait_for_threads(thread_type="remotes") and (
                    config.power
                    and cpiexec.wait_for_threads(name="_toggle_refresh")):
                log.show(
                    "Timeout Waiting for init or toggle threads to complete try again later or"
                    " investigate logs")
                return

        # -- // Update/Refresh Local Data (Adapters/Interfaces) \\ --
        local.data = local.build_local_dict(refresh=True)
        log.debugv(
            f"Final Data set collected for {local.hostname}: {local.data}")

        # -- // Get details from Google Drive - once populated will skip \\ --
        if not bypass_cloud and self.do_cloud and not self.local_only:
            if cloud_svc == "gdrive" and self.cloud is None:
                # burried import until I find out why this import takes so @#%$@#% long.  Not imported until 1st refresh is called
                with Halo(text="Loading Google Drive Library",
                          spinner="dots1"):
                    from consolepi.gdrive import GoogleDrive
                self.cloud = GoogleDrive(hostname=local.hostname)
                log.info("[MENU REFRESH] Gdrive init")

            # Pass Local Data to update_sheet method get remotes found on sheet as return
            # update sheets function updates local_cloud_file
            _msg = "[MENU REFRESH] Updating to/from {}".format(cloud_svc)
            log.info(_msg)
            if stdin.isatty():
                self.spin.start(_msg)
            # -- // SYNC DATA WITH GDRIVE \\ --
            remote_consoles = self.cloud.update_files(
                local.data)  # local data refreshed above
            if remote_consoles and "Gdrive-Error:" not in remote_consoles:
                if stdin.isatty():
                    self.spin.succeed(_msg +
                                      "\n\tFound {} Remotes via Gdrive Sync".
                                      format(len(remote_consoles)))
                    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"] = self.convert_adapters(
                                    remote_consoles[r]["adapters"])
                            log.warning(
                                f"Adapter data for {r} retrieved from cloud in old API format... Converted"
                            )
            elif "Gdrive-Error:" in remote_consoles:
                if stdin.isatty():
                    self.spin.fail("{}\n\t{} {}".format(
                        _msg, self.log_sym_error, remote_consoles))
                log.show(remote_consoles
                         )  # display error returned from gdrive module
                remote_consoles = []
            else:
                if stdin.isatty():
                    self.spin.warn(_msg +
                                   "\n\tNo Remotes Found via Gdrive Sync")

            if len(remote_consoles) > 0:
                _msg = f"[MENU REFRESH] Updating Local Cache with data from {cloud_svc}"
                log.info(_msg)
                if stdin.isatty():
                    self.spin.start(_msg)
                self.update_local_cloud_file(remote_consoles)
                if stdin.isatty():
                    self.spin.succeed(_msg)  # no real error correction here
            else:
                log.warning(
                    f"[MENU REFRESH] No Remote ConsolePis found on {cloud_svc}",
                    show=True,
                )
        else:
            if self.do_cloud and not bypass_cloud:
                log.show(
                    f"Not Updating from {cloud_svc} due to connection failure\n"
                    "Close and re-launch menu if network access has been restored"
                )

        # Update Remote data with data from local_cloud cache / cloud
        self.data = self.get_remote(data=remote_consoles)
Ejemplo n.º 12
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.'
Ejemplo n.º 13
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'
Ejemplo n.º 14
0
    def get_outlets_from_file(self):
        '''Get outlets defined in config

        returns:
            dict: with following keys (all values are dicts)
                linked: linked outlets from config (linked to serial adapters- auto pwr-on)
                dli_power: dict any dlis in config have all ports represented here
                failures: failure to connect to any outlets will result in an entry here
                    outlet_name: failure description
        '''
        outlet_data = self.cfg_yml.get('POWER')
        if not outlet_data:  # fallback to legacy json config
            outlet_data = self.get_json_file(self.static.get('POWER_FILE'))

        if not outlet_data:
            if self.power:
                log.show('Power Function Disabled - Configuration Not Found')
                self.power = False
            self.outlet_types = []
            return outlet_data

        types = []
        by_dev: Dict[str, Any] = {}
        for k in outlet_data:
            _type = outlet_data[k].get('type').lower()
            relays = [] if _type != "esphome" else utils.listify(
                outlet_data[k].get('relays', k))
            linked = outlet_data[k].get('linked_devs', {})

            if linked:
                outlet_data[k]['linked_devs'] = utils.format_dev(
                    outlet_data[k]['linked_devs'],
                    hosts=self.hosts,
                    with_path=True)
                self.linked_exists = True
                for dev in outlet_data[k]['linked_devs']:
                    if _type == 'dli':
                        self.do_dli_menu = True
                        _this = [
                            f"{k}:{[int(p) for p in utils.listify(outlet_data[k]['linked_devs'][dev])]}"
                        ]
                    elif _type == 'esphome':
                        _linked = utils.listify(
                            outlet_data[k]['linked_devs'][dev])
                        _this = [f'{k}:{[p for p in _linked]}']
                    else:
                        _this = [k]
                    by_dev[dev] = _this if dev not in by_dev else by_dev[
                        dev] + _this
            else:
                outlet_data[k]['linked_devs'] = {}

            if outlet_data[k]["type"].lower() not in types:
                types.append(outlet_data[k]["type"].lower())

            if outlet_data[k]['type'].upper() == 'GPIO' and isinstance(outlet_data[k].get('address'), str) \
                    and outlet_data[k]['address'].isdigit():
                outlet_data[k]['address'] = int(outlet_data[k]['address'])

            # This block determines if we should show dli_menu / if any esphome outlets match criteria to show
            # in dli menu (anytime it has exactly 8 outlets, if it has > 1 relay and not all are linked)
            if not self.do_dli_menu and _type == "esphome" and len(relays) > 1:
                if len(relays) == 8 or not linked:
                    self.do_dli_menu = True
                elif [r for r in relays if f"'{r}'" not in str(linked)]:
                    self.do_dli_menu = True

        self.outlet_types = types
        outlet_data = {
            'defined': outlet_data,
            'linked': by_dev,
            'dli_power': {},
            'esp_power': {},
            'failures': {}
        }

        return outlet_data