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
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'
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
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.'
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
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'
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
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):