def test_values_not_all_iterables(self): # Make sure we don't confuse other sequence types, e.g. str with self.assertRaises(TypeError) as exception_context: PrefixList("zero") self.assertEqual( str(exception_context.exception), "Legal values should be provided via an iterable of strings, " "got 'zero'.")
# * 'info': A temporary, frameless popup dialog that immediately updates the # object and is active only while the dialog is still over the invoking # control. # * 'wizard': A wizard modal dialog box. A wizard contains a sequence of # pages, which can be accessed by clicking **Next** and **Back** buttons. # Changes to attribute values are applied only when the user clicks the # **Finish** button on the last page. AKind = PrefixList( ( "panel", "subpanel", "modal", "nonmodal", "livemodal", "live", "popup", "popover", "info", "wizard", ), default_value='live', desc="the kind of view window to create", cols=4, ) # Apply changes handler: OnApply = Callable( desc="the routine to call when modal changes are applied " "or reverted" ) # Is the dialog window resizable?
class Person(HasTraits): atr1 = PrefixList(('yes', 'no')) atr2 = PrefixList(['yes', 'no'])
class Annotater(Component): color = ColorTrait((0.0, 0.0, 0.0, 0.2)) style = PrefixList(["rectangular", "freehand"], default_value="rectangular") annotation = Event traits_view = View( Group("<component>", id="component"), Group("<links>", id="links"), Group("color", "style", id="annotater", style="custom"), ) # ------------------------------------------------------------------------- # Mouse event handlers # ------------------------------------------------------------------------- def _left_down_changed(self, event): event.handled = True self.window.mouse_owner = self self._cur_x, self._cur_y = event.x, event.y self._start_x, self._start_y = event.x, event.y def _left_up_changed(self, event): event.handled = True self.window.mouse_owner = None if self.xy_in_bounds(event): self.annotation = ( min(self._start_x, event.x), min(self._start_y, event.y), abs(self._start_x - event.x), abs(self._start_y - event.y), ) self._start_x = self._start_y = self._cur_x = self._cur_y = None self.redraw() def _mouse_move_changed(self, event): event.handled = True if self._start_x is not None: x = max(min(event.x, self.right - 1.0), self.x) y = max(min(event.y, self.top - 1.0), self.y) if (x != self._cur_x) or (y != self._cur_y): self._cur_x, self._cur_y = x, y self.redraw() # ------------------------------------------------------------------------- # "Component" interface # ------------------------------------------------------------------------- def _draw(self, gc): "Draw the contents of the control" if self._start_x is not None: with gc: gc.set_fill_color(self.color_) gc.begin_path() gc.rect( min(self._start_x, self._cur_x), min(self._start_y, self._cur_y), abs(self._start_x - self._cur_x), abs(self._start_y - self._cur_y), ) gc.fill_path() return
# ----------------------------------------------------------------------------- # Font trait: font_trait = KivaFont(default_font_name) # Bounds trait bounds_trait = CList([0.0, 0.0]) # (w,h) coordinate_trait = CList([0.0, 0.0]) # (x,y) # Component minimum size trait # PZW: Make these just floats, or maybe remove them altogether. ComponentMinSize = Range(0.0, 99999.0) ComponentMaxSize = ComponentMinSize(99999.0) # Pointer shape trait: Pointer = PrefixList(pointer_shapes, default_value="arrow") # Cursor style trait: cursor_style_trait = PrefixMap(cursor_styles, default_value="default") spacing_trait = Range(0, 63, value=4) padding_trait = Range(0, 63, value=4) margin_trait = Range(0, 63) border_size_trait = Range(0, 8, editor=border_size_editor) # Time interval trait: TimeInterval = Union(None, Range(0.0, 3600.0)) # Stretch traits: Stretch = Range(0.0, 1.0, value=1.0) NoStretch = Stretch(0.0)
List, PrefixList, Range, Str, TraitError, TraitType, ) from .helper import SequenceTypes # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # Orientation trait: Orientation = PrefixList(("vertical", "horizontal")) # Styles for user interface elements: EditorStyle = style_trait = PrefixList( ("simple", "custom", "text", "readonly"), cols=4) # Group layout trait: Layout = PrefixList(("normal", "split", "tabbed", "flow", "fold")) # Trait for the default object being edited: AnObject = Expression("object") # The default dock style to use: DockStyle = dock_style_trait = Enum( "fixed", "horizontal",
class Shell(HasRequiredTraits): host = Str(value="", required=True) port = Range(value=22, low=1, high=65534) username = Str(value=getuser(), required=False) password = Str(value="", required=False) private_key_path = File(value=os.path.expanduser("~/.ssh/id_rsa")) original_host = Str(value="", required=False) inventory_file = File(required=False, default=None) # FIXME - needs more drivers... -> https://exscript.readthedocs.io/en/latest/Exscript.protocols.drivers.html driver = PrefixList( value="generic", values=["generic", "shell", "junos", "ios"], required=False ) termtype = PrefixList( value="dumb", values=["dumb", "xterm", "vt100"], required=False ) protocol = PrefixList(value="ssh", values=["ssh"], required=False) stdout = PrefixList(value=None, values=[None, sys.stdout], required=False) stderr = PrefixList(value=sys.stderr, values=[None, sys.stderr], required=False) banner_timeout = Range(value=20, low=1, high=30, required=False) connect_timeout = Range(value=10, low=1, high=30, required=False) prompt_timeout = Range(value=10, low=1, high=65535, required=False) prompt_list = List(Str, required=False) default_prompt_list = List(re.Pattern, required=False) account = Any(value=None, required=True) account_list = List(Exscript.account.Account, required=False) json_logfile = File(value="/dev/null", required=False) jh = Any(value=None) encoding = PrefixList(value="utf-8", values=["latin-1", "utf-8"], required=False) interact = Bool(value=False, values=[True, False]) downgrade_ssh_crypto = Bool(value=False, values=[True, False]) ssh_attempt_number = Range(value=1, low=1, high=3, required=False) conn = Any(required=False) debug = Range(value=0, low=0, high=5, required=False) allow_invalid_command = Bool(value=True, values=[True, False], required=False) MAX_SSH_ATTEMPT = Int(3) def __init__(self, **traits): super().__init__(**traits) self.original_host = copy.copy(self.host) assert self.host != "" assert isinstance(self.port, int) assert len(self.account_list) == 0 if isinstance(self.account, Account): self.append_account(self.account) else: raise ValueError("Account must be included in the Shell() call") # Ensure this was NOT called with username if traits.get("username", False) is not False: raise ValueError("Shell() calls with username are not supported") # Ensure this was NOT called with password if traits.get("password", False) is not False: raise ValueError("Shell() calls with password are not supported") # Check whether host matches an ip address in the inventory... # Overwrite self.host with resolved IP Address... resolved_host = self.search_inventory_for_host_address(self.host) logger.debug( "Shell(host='%s') resolved host to '%s'" % (self.original_host, resolved_host) ) self.host = resolved_host self.conn = self.do_ssh_login(debug=self.debug) self.allow_invalid_command = True # Always store the original prompt(s) so we can fallback to them later self.default_prompt_list = self.conn.get_prompt() # Populate the initial prompt list... if self.json_logfile != "/dev/null": self.open_json_log() self.json_log_entry(cmd="ssh2", action="login", timeout=False) if self.interact is True: self.interactive_session() def __repr__(self): return """<Shell: %s>""" % self.host def interactive_session(self): """Basic method to run an interactive session TODO - write timestamped interactive session to disk... """ finished = False while finished is not True: cmd = input("{0}# ".format(self.original_host)) for line in cmd.splitlines(): self.execute(line, consume=False, timeout=10) for line in self.conn.response.splitlines(): print(line.strip()) def search_inventory_for_host_address(self, hostname=None): """Use an ansible-style inventory file to map the input hostname to an IPv4 or IPv6 address""" logger.debug("Checking inventory for hostname='%s'" % hostname) # Search inventory first... for line in self.iter_inventory_lines(): # Try to resolve the address as an ansible hostfile... mm = re.search( r"^\s*({0})\s.*?(ansible_host)\s*=\s*(\S+)".format(hostname.lower()), line.lower(), ) if mm is not None: host_ip = mm.group(3) tmp = stdlib_ip_factory(host_ip) # This will raise a ValueError logger.debug( "Found inventory match for '%s' -> %s" % (hostname, host_ip) ) return host_ip else: logger.debug("No inventory match for '%s'" % hostname) # If this resolves to a valid ipv4 string, return the ipv4 string valid_ipv4 = sock.getaddrinfo(hostname, None, sock.AF_INET)[0][4][0] logger.debug(valid_ipv4) try: if stdlib_ip_factory(valid_ipv4): return valid_ipv4 except ValueError: logger.debug("No ipv4 DNS record for '%s'" % hostname) # Fall through to the next step pass # If this resolves to a valid ipv6 string, return the ipv6 string valid_ipv6 = sock.getaddrinfo(hostname, None, sock.AF_INET6)[0][4][0] logger.debug(valid_ipv6) try: if stdlib_ip_factory(valid_ipv6): logger.debug("No ipv6 DNS record for '%s'" % hostname) return valid_ipv6 except ValueError: # Fall through to the next step pass # Raise a non-fatal error... logger.warning( "Could not resolve or match hostname='%s' in the inventory file: %s" % (hostname, self.inventory_file) ) return None def iter_inventory_lines(self): explicit_inventory_filepath = os.path.expanduser(self.inventory_file) # Handle the default invntory value... if self.inventory_file.strip() == "": return [] if os.path.isfile(explicit_inventory_filepath): with open(explicit_inventory_filepath, "r", encoding="utf=8") as fh: for line in fh.read().splitlines(): yield line.lower() else: raise OSError("Cannot find inventory file named '%s'" % self.inventory_file) def do_ssh_login( self, connect_timeout=10, debug=0, ): assert isinstance(connect_timeout, int) assert isinstance(debug, int) assert len(self.account_list) > 0 # FIXME - clean up PrivateKey here... private_key = PrivateKey(keytype="rsa").from_file(self.private_key_path) self.downgrade_ssh_crypto = False if self.protocol == "ssh": for self.ssh_attempt_number in [1, 2, 3]: assert self.ssh_attempt_number <= self.MAX_SSH_ATTEMPT # You have to change allowed ciphers / key exchange options # **before** the connection # -> https://stackoverflow.com/a/31303321/667301 if self.downgrade_ssh_crypto is True: paramiko.Transport._preferred_ciphers = ( "aes128-cbc", "3des-cbc", ) paramiko.Transport._preferred_kex = ( "diffie-hellman-group-exchange-sha1", "diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1", ) conn = SSH2(driver=self.driver) # Save default values... DEFAULT_CONNECT_TIMEOUT = conn.get_connect_timeout() DEFAULT_PROMPT_LIST = conn.get_prompt() DEFAULT_PROMPT_TIMEOUT = conn.get_timeout() # FIXME - Exscript should be handling this but the pypi pkg doesn't # conn.set_connect_timeout(connect_timeout) try: conn.connect(hostname=self.host, port=self.port) break except sock.timeout as ee: self.downgrade_ssh_crypto = True if self.ssh_attempt_number == self.MAX_SSH_ATTEMPT: error = "Timeout connecting to TCP port {1} on host:{0}".format( self.host, self.port ) logger.critical(error) raise OSError(error) else: assert self.ssh_attempt_number < self.MAX_SSH_ATTEMPT time.sleep(0.5) except SSHException as ee: self.downgrade_ssh_crypto = True if self.ssh_attempt_number == self.MAX_SSH_ATTEMPT: error = ( "Connection to host:{0} on TCP port {1} was reset".format( self.host, self.port ) ) logger.critical(error) raise OSError(error) else: assert self.ssh_attempt_number < self.MAX_SSH_ATTEMPT time.sleep(0.5) login_success = False for account in self.account_list: conn.login(account) try: assert isinstance(conn, SSH2) # This succeeds if logged in... login_success = True break except AssertionError as aa: # login with account failed... continue assert login_success is True if login_success is True: self.password = account.password else: raise ValueError("Login to host='%s' failed" % self.host) conn.set_connect_timeout(DEFAULT_CONNECT_TIMEOUT) return conn else: raise ValueError("FATAL: proto='%s' isn't a valid protocol" % proto) ## TODO - we should use a true try / except here... def open_json_log(self): if self.json_logfile == "/dev/null": return None else: self.jh = open(os.path.expanduser(self.json_logfile), "w", encoding="utf-8") atexit.register(self.close_json_log) return True ## TODO - we should use a true try / except here... def close_json_log(self): if self.json_logfile == "/dev/null": return None else: self.jh.flush() self.jh.close() return True def json_log_entry(self, cmd=None, action=None, result=None, timeout=False): if self.json_logfile == "/dev/null": return None assert isinstance(cmd, str) assert isinstance(action, str) assert action in set(["login", "execute", "send", "expect", "output"]) assert isinstance(result, str) or (result is None) assert isinstance(timeout, bool) # Pretty json output... or reference json docs # https://stackoverflow.com/a/12944035/667301 self.jh.write( json.dumps( { "time": str(arrow.now()), "cmd": cmd, "host": self.host, "action": action, "result": result, "timeout": timeout, }, indent=4, sort_keys=True, ) + "," + os.linesep ) def append_account(self, account): # From the exscript docs... # key = PrivateKey.from_file('~/.ssh/id_rsa', 'my_key_password') self.account_list.append(account) def _extend_prompt(self, prompt_list=()): retval = list() for ii in prompt_list: if isinstance(ii, str): compiled = re.compile(ii) retval.append(compiled) elif isinstance(ii, re.Pattern): retval.append(ii) else: raise ValueError("Cannot process prompt:'%s'" % ii) for ii in self.conn.get_prompt(): retval.append(ii) self.conn.set_prompt(retval) def tfsm(self, template=None, input_str=None): """Run the textfsm template against input_str""" assert isinstance(template, str) assert isinstance(input_str, str) if os.path.isfile(os.path.expanduser(str(template))): # open the textfsm template from disk... fh = open(template, "r") else: # build a fake filehandle around textfsm template string fh = StringIO(template) fsm = TextFSM(fh) header = fsm.header values = fsm.ParseText(input_str) assert values != [], "Could not match any values with the template." ## Pack the extracted values into a list of dicts, using keys from ## the header file retval = list() # Values is a nested list of captured information for ii in (values, values[0]): if not isinstance(ii, list): continue for row in ii: try: # Require the row to be a list assert isinstance(row, list) # Require row to be exactly as long as the header list assert len(row) == len(header) row_dict = {} for idx, value in enumerate(row): row_dict[header[idx]] = value retval.append(row_dict) except AssertionError: break if len(retval) > 0: return retval else: raise ValueError("Cannot parse the textfsm template") def execute( self, cmd="", prompt_list=(), timeout=0, template=None, debug=0, consume=True ): assert isinstance(cmd, str) if cmd.strip() == "": assert len(cmd.splitlines()) == 0 else: assert len(cmd.splitlines()) == 1 assert isinstance(timeout, int) assert (template is None) or isinstance(template, str) assert isinstance(debug, int) cmd = cmd.strip() if debug > 0: logger.debug("Calling execute(cmd='%s', timeout=%s)" % (cmd, timeout)) if len(prompt_list) > 0: self._extend_prompt(prompt_list) normal_timeout = self.conn.get_timeout() if timeout > 0: self.conn.set_timeout(timeout) # Handle prompt_list... self.set_custom_prompts(prompt_list) self.json_log_entry(cmd=cmd, action="execute", timeout=False) # Handle sudo command... if cmd.strip()[0:4] == "sudo": pre_sudo_prompts = self.conn.get_prompt() # FIXME I removed re.compile from the sudo prompt. Example prompt: # [sudo] password for mpenning: sudo_prompt = re.compile(r"[\r\n].+?:") prompts_w_sudo = copy.deepcopy(pre_sudo_prompts) prompts_w_sudo.insert(0, sudo_prompt) self.conn.set_prompt(prompts_w_sudo) # Sending sudo cmd here... self.conn.send(cmd + os.linesep) prompt_idx, re_match_object = self.conn.expect_prompt() # idx==0 is a sudo password prompt... if prompt_idx == 0: self.conn.set_prompt(self.default_prompt_list) self.conn.send(self.password + os.linesep) prompt_idx, re_match_object = self.conn.expect_prompt() assert isinstance(prompt_idx, int) else: raise ValueError("Cannot complete 'execute(cmd='%s')" % cmd) self.conn.set_prompt(pre_sudo_prompts) # Handle non-sudo execute()... else: try: self.conn.execute(cmd) except ConnectionResetError as dd: error = "SSH session with {0} was reset while running cmd='{1}'".format( self.host, cmd ) raise ConnectionResetError(error) except InvalidCommandException: if self.allow_invalid_command is False: error = "cmd='%s' is an invalid command" % cmd logger.critical(error) raise InvalidCommandException(error) # Reset the prompt list at the end of the command... if len(prompt_list) > 0: self.reset_prompt() # Reset the timeout at the end of the command... if self.conn.get_timeout() != normal_timeout: self.conn.set_timeout(normal_timeout) # save the raw response... cmd_output = self.conn.response self.json_log_entry(cmd=cmd, action="output", result=cmd_output, timeout=False) ## TextFSM ## If template is specified, parse the response into a list of dicts... if isinstance(template, str): return self.tfsm(template, cmd_output) else: return cmd_output def send(self, cmd="", debug=0): assert isinstance(cmd, str) assert len(cmd.splitlines()) == 1 assert isinstance(debug, int) self.json_log_entry(cmd=cmd, action="send", result=None, timeout=False) self.conn.send(cmd) def expect(self, prompt_list=(), timeout=0, debug=0): """Expect prompts, including those in prompt_list""" assert isinstance(debug, int) if debug > 0: pass normal_timeout = self.conn.get_timeout() if timeout > 0: self.conn.set_timeout(timeout) normal_prompts = self.conn.get_prompt() # Handle prompt_list... self.set_custom_prompts(prompt_list) prompt_idx, re_match_object = self.conn.expect_prompt() self.conn.set_timeout(normal_timeout) # Reset the prompt list at the end of the command... if len(prompt_list) > 0: self.reset_prompt() self.json_log_entry(cmd=cmd, action="expect", result=None, timeout=False) return prompt_idx, re_match_object def set_timeout(self, timeout=0): """Set the command timeout""" if isinstance(timeout, int) and timeout > 0: return self.conn.set_timeout(timeout) def set_custom_prompts(self, prompt_list=()): """Wrapper around set_prompt()""" return self.set_prompt(prompt_list) def reset_prompt(self): """Reset all prompts to default""" return self.conn.set_prompt(self.default_prompt_list) def set_prompt(self, prompt_list=()): """Extend the expected prompts with prompts in prompt_list""" normal_prompts = self.conn.get_prompt() custom_prompts = copy.copy(normal_prompts) if (isinstance(prompt_list, tuple) or isinstance(prompt_list, list)) and len( prompt_list ) > 0: prompt_list.reverse() for prompt in prompt_list: if isinstance(prompt, str): custom_prompts.insert(0, re.compile(prompt)) elif isinstance(prompt, re.Pattern): custom_prompts.insert(0, prompt) else: raise ValueError("Cannot process prompt='%s'" % prompt) self.conn.set_prompt(custom_prompts) return normal_prompts, custom_prompts def close(self, force=True, timeout=0, debug=0): self.conn.close(force=force) @property def response(self): return self.conn.response
class A(HasTraits): foo = PrefixList(["zero", "one", "two"], default_value="one")
def test_values_is_empty(self): # it doesn't make sense to use a PrefixList with an empty list, so make # sure we raise a ValueError with self.assertRaises(ValueError): PrefixList([])
def test_values_not_sequence(self): # Defining values with this signature is not supported with self.assertRaises(TypeError): PrefixList("zero", "one", "two")
class A(HasTraits): foo = PrefixList(("abc1", "abc2"))