Ejemplo n.º 1
0
    def _setup_auth(
        auth_private_key: str,
        auth_strict_key: bool,
        auth_bypass: bool,
    ) -> Tuple[str, bool, bool]:
        """
        Parse and setup auth attributes

        Args:
            auth_private_key: ssh key to parse/set
            auth_strict_key: strict key to parse/set
            auth_bypass: bypass to parse/set

        Returns:
            Tuple[str, bool, bool]: string of private key path, bool for auth_strict_key, and bool
                for auth_bypass values

        Raises:
            ScrapliTypeError: if auth_strict_key is not a bool
            ScrapliTypeError: if auth_bypass is not a bool

        """
        if not isinstance(auth_strict_key, bool):
            raise ScrapliTypeError(f"`auth_strict_key` should be bool, got {type(auth_strict_key)}")
        if not isinstance(auth_bypass, bool):
            raise ScrapliTypeError(f"`auth_bypass` should be bool, got {type(auth_bypass)}")

        if auth_private_key:
            auth_private_key_path = resolve_file(file=auth_private_key)
        else:
            auth_private_key_path = ""

        return auth_private_key_path, auth_strict_key, auth_bypass
Ejemplo n.º 2
0
    def _setup_callables(
        self,
        on_init: Optional[Callable[..., Any]],
        on_open: Optional[Callable[..., Any]],
        on_close: Optional[Callable[..., Any]],
    ) -> None:
        """
        Parse and setup callables (on_init/on_open/on_close)

        Args:
            on_init: on_init to parse/set
            on_open: on_open to parse/set
            on_close: on_close to parse/set

        Returns:
            None

        Raises:
            ScrapliTypeError: if any of the on_* methods are not callables (or None)

        """
        if on_init is not None and not callable(on_init):
            raise ScrapliTypeError(f"`on_init` must be a callable, got {type(on_init)}")
        if on_open is not None and not callable(on_open):
            raise ScrapliTypeError(f"`on_open` must be a callable, got {type(on_open)}")
        if on_close is not None and not callable(on_close):
            raise ScrapliTypeError(f"`on_close` must be a callable, got {type(on_close)}")

        self.on_init = on_init
        self.on_open = on_open
        self.on_close = on_close
Ejemplo n.º 3
0
    def _setup_ssh_file_args(
        self,
        transport: str,
        ssh_config_file: Union[str, bool],
        ssh_known_hosts_file: Union[str, bool],
    ) -> Tuple[str, str]:
        """
        Parse and setup ssh related arguments

        Args:
            transport: string name of selected transport (so we can ignore this if transport
                contains "telnet" in the name)
            ssh_config_file: string to path for ssh config file, True to use default ssh config file
                or False to ignore default ssh config file
            ssh_known_hosts_file: string to path for ssh known hosts file, True to use default known
                file locations. Only applicable/needed if `auth_strict_key` is set to True

        Returns:
            Tuple[str, str]: string path to config file, string path to known hosts file

        Raises:
            ScrapliTypeError: if invalid config file or known hosts file value provided

        """
        if "telnet" in transport:
            self.logger.debug(
                "telnet-based transport selected, ignoring ssh file arguments")
            # the word "telnet" should occur in all telnet drivers, always. so this should be safe!
            return "", ""

        if not isinstance(ssh_config_file, (str, bool)):
            raise ScrapliTypeError(
                f"`ssh_config_file` must be str or bool, got {type(ssh_config_file)}"
            )
        if not isinstance(ssh_known_hosts_file, (str, bool)):
            raise ScrapliTypeError(
                "`ssh_known_hosts_file` must be str or bool, got "
                f"{type(ssh_known_hosts_file)}")

        if ssh_config_file is not False:
            if isinstance(ssh_config_file, bool):
                cfg = ""
            else:
                cfg = ssh_config_file
            resolved_ssh_config_file = self._resolve_ssh_config(cfg)
        else:
            resolved_ssh_config_file = ""

        if ssh_known_hosts_file is not False:
            if isinstance(ssh_known_hosts_file, bool):
                known_hosts = ""
            else:
                known_hosts = ssh_known_hosts_file
            resolved_ssh_known_hosts_file = self._resolve_ssh_known_hosts(
                known_hosts)
        else:
            resolved_ssh_known_hosts_file = ""

        return resolved_ssh_config_file, resolved_ssh_known_hosts_file
Ejemplo n.º 4
0
    def _pre_send_command(
        host: str, command: str, failed_when_contains: Optional[Union[str, List[str]]] = None
    ) -> Response:
        """
        Handle pre "send_command" tasks for consistency between sync/async versions

        Args:
            host: string name of the host
            command: string to send to device in privilege exec mode
            failed_when_contains: string or list of strings indicating failure if found in response

        Returns:
            Response: Scrapli Response object

        Raises:
            ScrapliTypeError: if command is anything but a string

        """
        if not isinstance(command, str):
            raise ScrapliTypeError(
                f"`send_command` expects a single string, got {type(command)}, "
                "to send a list of commands use the `send_commands` method instead."
            )

        return Response(
            host=host,
            channel_input=command,
            failed_when_contains=failed_when_contains,
        )
Ejemplo n.º 5
0
    def _pre_send_configs(
        self,
        configs: List[str],
        failed_when_contains: Optional[Union[str, List[str]]] = None,
        privilege_level: str = "",
    ) -> Tuple[str, Union[str, List[str]]]:
        """
        Handle pre "send_configs" tasks for consistency between sync/async versions

        Args:
            configs: list of strings to send to device in config mode
            failed_when_contains: string or list of strings indicating failure if found in response
            privilege_level: name of configuration privilege level/type to acquire; this is platform
                dependent, so check the device driver for specifics. Examples of privilege_name
                would be "configuration_exclusive" for IOSXRDriver, or "configuration_private" for
                JunosDriver. You can also pass in a name of a configuration session such as
                "my-config-session" if you have registered a session using the
                "register_config_session" method of the EOSDriver or NXOSDriver.

        Returns:
            Tuple[str, Union[str, List[str]]]: string of resolved privilege level name, and failed
                when contains which may be a string or list of strings

        Raises:
            ScrapliTypeError: if configs is anything but a list
            ScrapliPrivilegeError: if connection is in 'generic_driver_mode' -- this should be a
                non-standard use case so there is no reason to complicate the config(s) methods
                with supporting generic driver mode (plus if there was config modes in generic
                driver mode that wouldn't be very generic driver like, would it!)

        """
        if not isinstance(configs, list):
            raise ScrapliTypeError(
                f"'send_configs' expects a list of strings, got {type(configs)}, "
                "to send a single configuration line/string use the 'send_config' method instead."
            )

        if self._generic_driver_mode is True:
            raise ScrapliPrivilegeError(
                "connection is in 'generic_driver_mode', send config(s|s_from_file) is disabled"
            )

        if failed_when_contains is None:
            final_failed_when_contains = self.failed_when_contains
        elif isinstance(failed_when_contains, str):
            final_failed_when_contains = [failed_when_contains]
        else:
            final_failed_when_contains = failed_when_contains

        if privilege_level:
            self._validate_privilege_level_name(
                privilege_level_name=privilege_level)
            resolved_privilege_level = privilege_level
        else:
            resolved_privilege_level = "configuration"

        return resolved_privilege_level, final_failed_when_contains
Ejemplo n.º 6
0
    def _pre_send_inputs_interact(interact_events: List[Tuple[str, str, Optional[bool]]]) -> None:
        """
        Handle pre "send_inputs_interact" tasks for consistency between sync/async versions

        Args:
            interact_events: interact events passed to `send_inputs_interact`

        Returns:
            None

        Raises:
            ScrapliTypeError: if input is anything but a string

        """
        if not isinstance(interact_events, list):
            raise ScrapliTypeError(f"`interact_events` expects a List, got {type(interact_events)}")
Ejemplo n.º 7
0
    def _pre_send_input(channel_input: str) -> None:
        """
        Handle pre "send_input" tasks for consistency between sync/async versions

        Args:
            channel_input: string input to send to channel

        Returns:
            bytes: current channel buffer

        Raises:
            ScrapliTypeError: if input is anything but a string

        """
        if not isinstance(channel_input, str):
            raise ScrapliTypeError(
                f"`send_input` expects a single string, got {type(channel_input)}."
            )
Ejemplo n.º 8
0
    def _pre_send_commands(commands: List[str]) -> MultiResponse:
        """
        Handle pre "send_command" tasks for consistency between sync/async versions

        Args:
            commands: list of strings to send to device in privilege exec mode

        Returns:
            MultiResponse: Scrapli MultiResponse object

        Raises:
            ScrapliTypeError: if command is anything but a string

        """
        if not isinstance(commands, list):
            raise ScrapliTypeError(
                f"`send_commands` expects a list of strings, got {type(commands)}, "
                "to send a single command use the `send_command` method instead."
            )

        return MultiResponse()
Ejemplo n.º 9
0
    def _setup_host(host: str, port: int) -> Tuple[str, int]:
        """
        Parse and setup host attributes

        Args:
            host: host to parse/set
            port: port to parse/set

        Returns:
            tuple: host, port -- host is stripped to ensure no weird whitespace floating around

        Raises:
            ScrapliValueError: if host is not provided
            ScrapliTypeError: if port is not an integer

        """
        if not host:
            raise ScrapliValueError("`host` should be a hostname/ip address, got nothing!")
        if not isinstance(port, int):
            raise ScrapliTypeError(f"`port` should be int, got {type(port)}")

        return host.strip(), port
Ejemplo n.º 10
0
    def _pre_send_from_file(file: str, caller: str) -> List[str]:
        """
        Handle pre "send_*_from_file" tasks for consistency between sync/async versions

        Args:
            file: string path to file
            caller: name of the calling method for more helpful error message

        Returns:
            list: list of commands/configs read from file

        Raises:
            ScrapliTypeError: if anything but a string is provided for `file`

        """
        if not isinstance(file, str):
            raise ScrapliTypeError(f"`{caller}` expects a string path to a file, got {type(file)}")
        resolved_file = resolve_file(file)

        with open(resolved_file, "r", encoding="utf-8") as f:
            commands = f.read().splitlines()

        return commands
Ejemplo n.º 11
0
    def _pre_send_config(config: str) -> List[str]:
        """
        Handle pre "send_config" tasks for consistency between sync/async versions

        Args:
            config: string configuration to send to the device, supports sending multi-line strings

        Returns:
            list: list of config lines from provided "config" input

        Raises:
            ScrapliTypeError: if anything but a string is provided for `file`

        """
        if not isinstance(config, str):
            raise ScrapliTypeError(
                f"'send_config' expects a single string, got {type(config)}, "
                "to send a list of configs use the 'send_configs' method instead."
            )

        # in order to handle multi-line strings, we split lines
        split_config = config.splitlines()

        return split_config
Ejemplo n.º 12
0
    def __new__(  # pylint: disable=R0914
        cls,
        platform: str,
        host: str,
        privilege_levels: Optional[Dict[str, PrivilegeLevel]] = None,
        default_desired_privilege_level: Optional[str] = None,
        port: Optional[int] = None,
        auth_username: Optional[str] = None,
        auth_password: Optional[str] = None,
        auth_private_key: Optional[str] = None,
        auth_private_key_passphrase: Optional[str] = None,
        auth_strict_key: Optional[bool] = None,
        auth_bypass: Optional[bool] = None,
        timeout_socket: Optional[float] = None,
        timeout_transport: Optional[float] = None,
        timeout_ops: Optional[float] = None,
        comms_return_char: Optional[str] = None,
        ssh_config_file: Optional[Union[str, bool]] = None,
        ssh_known_hosts_file: Optional[Union[str, bool]] = None,
        on_init: Optional[Callable[..., Any]] = None,
        on_open: Optional[Callable[..., Any]] = None,
        on_close: Optional[Callable[..., Any]] = None,
        transport: Optional[str] = None,
        transport_options: Optional[Dict[str, Any]] = None,
        channel_log: Optional[Union[str, bool, BytesIO]] = None,
        channel_log_mode: Optional[str] = None,
        channel_lock: Optional[bool] = None,
        logging_uid: Optional[str] = None,
        auth_secondary: Optional[str] = None,
        failed_when_contains: Optional[List[str]] = None,
        textfsm_platform: Optional[str] = None,
        genie_platform: Optional[str] = None,
        variant: Optional[str] = None,
        **kwargs: Dict[Any, Any],
    ) -> "AsyncScrapli":
        r"""
        Scrapli Factory method for asynchronous drivers

        Args:
            platform: name of the scrapli platform to return a connection object for; should be
                one of the "core" platforms or a valid community platform name
            host: host ip/name to connect to
            port: port to connect to
            auth_username: username for authentication
            auth_private_key: path to private key for authentication
            auth_private_key_passphrase: passphrase for decrypting ssh key if necessary
            auth_password: password for authentication
            auth_strict_key: strict host checking or not
            auth_bypass: bypass "in channel" authentication -- only supported with telnet,
                asynctelnet, and system transport plugins
            timeout_socket: timeout for establishing socket/initial connection in seconds
            timeout_transport: timeout for ssh|telnet transport in seconds
            timeout_ops: timeout for ssh channel operations
            comms_return_char: character to use to send returns to host
            ssh_config_file: string to path for ssh config file, True to use default ssh config file
                or False to ignore default ssh config file
            ssh_known_hosts_file: string to path for ssh known hosts file, True to use default known
                file locations. Only applicable/needed if `auth_strict_key` is set to True
            on_init: callable that accepts the class instance as its only argument. this callable,
                if provided, is executed as the last step of object instantiation -- its purpose is
                primarily to provide a mechanism for scrapli community platforms to have an easy way
                to modify initialization arguments/object attributes without needing to create a
                class that extends the driver, instead allowing the community platforms to simply
                build from the GenericDriver or NetworkDriver classes, and pass this callable to do
                things such as appending to a username (looking at you RouterOS!!). Note that this
                is *always* a synchronous function (even for asyncio drivers)!
            on_open: callable that accepts the class instance as its only argument. this callable,
                if provided, is executed immediately after authentication is completed. Common use
                cases for this callable would be to disable paging or accept any kind of banner
                message that prompts a user upon connection
            on_close: callable that accepts the class instance as its only argument. this callable,
                if provided, is executed immediately prior to closing the underlying transport.
                Common use cases for this callable would be to save configurations prior to exiting,
                or to logout properly to free up vtys or similar
            transport: name of the transport plugin to use for the actual telnet/ssh/netconf
                connection. Available "core" transports are:
                    - system
                    - telnet
                    - asynctelnet
                    - ssh2
                    - paramiko
                    - asyncssh
                Please see relevant transport plugin section for details. Additionally third party
                transport plugins may be available.
            transport_options: dictionary of options to pass to selected transport class; see
                docs for given transport class for details of what to pass here
            channel_lock: True/False to lock the channel (threading.Lock/asyncio.Lock) during
                any channel operations, defaults to False
            channel_log: True/False or a string path to a file of where to write out channel logs --
                these are not "logs" in the normal logging module sense, but only the output that is
                read from the channel. In other words, the output of the channel log should look
                similar to what you would see as a human connecting to a device
            channel_log_mode: "write"|"append", all other values will raise ValueError,
                does what it sounds like it should by setting the channel log to the provided mode
            logging_uid: unique identifier (string) to associate to log messages; useful if you have
                multiple connections to the same device (i.e. one console, one ssh, or one to each
                supervisor module, etc.)
            failed_when_contains: list of strings indicating command/config failure
            textfsm_platform: string to use to fetch ntc-templates templates for textfsm parsing
            genie_platform: string to use to fetch genie parser templates
            privilege_levels: optional user provided privilege levels, if left None will default to
                scrapli standard privilege levels
            default_desired_privilege_level: string of name of default desired priv, this is the
                priv level that is generally used to disable paging/set terminal width and things
                like that upon first login, and is also the priv level scrapli will try to acquire
                for normal "command" operations (`send_command`, `send_commands`)
            auth_secondary: password to use for secondary authentication (enable)
            failed_when_contains: List of strings that indicate a command/config has failed
            variant: name of the community platform variant if desired
            **kwargs: should be unused, but here to accept any additional kwargs from users

        Returns:
            final_driver: asynchronous driver class for provided driver

        Raises:
            ScrapliValueError: if provided transport is asyncio
            ScrapliTypeError: if `platform` not in keyword arguments

        """
        logger.debug("AsyncScrapli factory initialized")

        if transport not in ASYNCIO_TRANSPORTS:
            raise ScrapliValueError("Use 'Scrapli' if using a synchronous transport!")

        if not isinstance(platform, str):
            raise ScrapliTypeError(f"Argument 'platform' must be 'str' got '{type(platform)}'")

        provided_kwargs = _build_provided_kwargs_dict(
            host=host,
            port=port,
            auth_username=auth_username,
            auth_password=auth_password,
            auth_private_key=auth_private_key,
            auth_private_key_passphrase=auth_private_key_passphrase,
            auth_strict_key=auth_strict_key,
            auth_bypass=auth_bypass,
            timeout_socket=timeout_socket,
            timeout_transport=timeout_transport,
            timeout_ops=timeout_ops,
            comms_return_char=comms_return_char,
            ssh_config_file=ssh_config_file,
            ssh_known_hosts_file=ssh_known_hosts_file,
            on_init=on_init,
            on_open=on_open,
            on_close=on_close,
            transport=transport,
            transport_options=transport_options,
            channel_log=channel_log,
            channel_log_mode=channel_log_mode,
            channel_lock=channel_lock,
            logging_uid=logging_uid,
            privilege_levels=privilege_levels,
            default_desired_privilege_level=default_desired_privilege_level,
            auth_secondary=auth_secondary,
            failed_when_contains=failed_when_contains,
            textfsm_platform=textfsm_platform,
            genie_platform=genie_platform,
            **kwargs,
        )

        final_driver, additional_kwargs = cls._get_driver(platform=platform, variant=variant)

        # at this point will need to merge the additional kwargs in (for community drivers),
        # ensure that kwargs passed by user supersede the ones coming from community platform
        if additional_kwargs:
            final_kwargs = {**additional_kwargs, **provided_kwargs}
        else:
            final_kwargs = provided_kwargs

        final_conn = final_driver(**final_kwargs)
        # cast the final conn to type Scrapli to appease mypy -- we know it will be a NetworkDriver
        # or GenericDriver, but thats ok =)
        final_conn = cast(AsyncScrapli, final_conn)
        return final_conn
Ejemplo n.º 13
0
    def __init__(self, ssh_config_file: str) -> None:
        """
        Initialize SSHConfig Object

        Parse OpenSSH config file

        Try to load the following data for all entries in config file:
            Host
            HostName
            Port
            User
            *AddressFamily
            *BindAddress
            *ConnectTimeout
            IdentitiesOnly
            IdentityFile
            *KbdInteractiveAuthentication
            *PasswordAuthentication
            *PreferredAuthentications

        * items are mostly ready to load but are unused in scrapli right now so are not being set
        at this point.

        NOTE: this does *not* accept duplicate "*" entries -- the final "*" entry will overwrite any
        previous "*" entries. In general for system transport this shouldn't matter much because
        scrapli only cares about parsing the config file to see if a key (any key) exists for a
        given host (we care about that because ideally we use "pipes" auth, but this is only an
        option if we have a key to auth with).

        Args:
            ssh_config_file: string path to ssh configuration file

        Returns:
            None

        Raises:
            ScrapliTypeError: if non-string value provided for ssh_config_file

        """
        if not isinstance(ssh_config_file, str):
            raise ScrapliTypeError(
                f"`ssh_config_file` expected str, got {type(ssh_config_file)}")

        self.ssh_config_file = os.path.expanduser(ssh_config_file)
        if self.ssh_config_file:
            with open(self.ssh_config_file, "r", encoding="utf-8") as f:
                self.ssh_config = f.read()
            self.hosts = self._parse()
            if not self.hosts:
                self.hosts = {}
            if "*" not in self.hosts:
                self.hosts["*"] = Host()
                self.hosts["*"].hosts = "*"
        else:
            self.hosts = {}
            self.hosts["*"] = Host()
            self.hosts["*"].hosts = "*"

        # finally merge all args from less specific hosts into the more specific hosts, preserving
        # the options from the more specific hosts of course
        self._merge_hosts()