mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
add ssh key authentication support, closes #81
This commit is contained in:
@@ -79,7 +79,10 @@ For HTTP devices (i.e. devices using [hyperglass-agent](https://github.com/check
|
||||
| Parameter | Type | Description |
|
||||
| :-------------- | :----- | :----------------------------------------------------------- |
|
||||
| <R/> `username` | String | Username |
|
||||
| <R/> `password` | String | Password <MiniNote>Passwords will never be logged</MiniNote> |
|
||||
| `password` | String | Password <MiniNote>Passwords will never be logged</MiniNote> |
|
||||
| `key` | Path | Path to SSH Private Key |
|
||||
|
||||
To use SSH key authentication, simply specify the path to the SSH private key with `key:`. If the key is encrypted, set the private key's password to the with the `password:` field, and hyperglass will use it to decrypt the SSH key.
|
||||
|
||||
### `ssl`
|
||||
|
||||
|
@@ -53,6 +53,8 @@ from hyperglass.configuration import params
|
||||
if params.debug:
|
||||
logging.getLogger("paramiko").setLevel(logging.DEBUG)
|
||||
|
||||
log.bind(logger_name="paramiko")
|
||||
|
||||
TUNNEL_TIMEOUT = 1.0 #: Timeout (seconds) for tunnel connection
|
||||
_DAEMON = False #: Use daemon threads in connections
|
||||
_CONNECTION_COUNTER = 1
|
||||
@@ -759,7 +761,7 @@ class SSHTunnelForwarder:
|
||||
host_pkey_directories=None, # look for keys in ~/.ssh
|
||||
gateway_timeout=None,
|
||||
*args,
|
||||
**kwargs # for backwards compatibility
|
||||
**kwargs, # for backwards compatibility
|
||||
):
|
||||
self.logger = logger or log
|
||||
self.ssh_host_key = ssh_host_key
|
||||
|
@@ -23,17 +23,29 @@ class SSHConnection(Connection):
|
||||
|
||||
def opener():
|
||||
"""Set up an SSH tunnel according to a device's configuration."""
|
||||
tunnel_kwargs = {
|
||||
"ssh_username": proxy.credential.username,
|
||||
"remote_bind_address": (self.device._target, self.device.port),
|
||||
"local_bind_address": ("localhost", 0),
|
||||
"skip_tunnel_checkup": False,
|
||||
"gateway_timeout": params.request_timeout - 2,
|
||||
}
|
||||
if proxy.credential._method == "password":
|
||||
# Use password auth if no key is defined.
|
||||
tunnel_kwargs[
|
||||
"ssh_password"
|
||||
] = proxy.credential.password.get_secret_value()
|
||||
else:
|
||||
# Otherwise, use key auth.
|
||||
tunnel_kwargs["ssh_pkey"] = proxy.credential.key.as_posix()
|
||||
if proxy.credential._method == "encrypted_key":
|
||||
# If the key is encrypted, use the password field as the
|
||||
# private key password.
|
||||
tunnel_kwargs[
|
||||
"ssh_private_key_password"
|
||||
] = proxy.credential.password.get_secret_value()
|
||||
try:
|
||||
return open_tunnel(
|
||||
proxy._target,
|
||||
proxy.port,
|
||||
ssh_username=proxy.credential.username,
|
||||
ssh_password=proxy.credential.password.get_secret_value(),
|
||||
remote_bind_address=(self.device._target, self.device.port),
|
||||
local_bind_address=("localhost", 0),
|
||||
skip_tunnel_checkup=False,
|
||||
gateway_timeout=params.request_timeout - 2,
|
||||
)
|
||||
return open_tunnel(proxy._target, proxy.port, **tunnel_kwargs)
|
||||
|
||||
except BaseSSHTunnelForwarderError as scrape_proxy_error:
|
||||
log.error(
|
||||
|
@@ -60,20 +60,35 @@ class NetmikoConnection(SSHConnection):
|
||||
|
||||
send_args = netmiko_nos_send_args.get(self.device.nos, {})
|
||||
|
||||
netmiko_args = {
|
||||
driver_kwargs = {
|
||||
"host": host or self.device._target,
|
||||
"port": port or self.device.port,
|
||||
"device_type": self.device.nos,
|
||||
"username": self.device.credential.username,
|
||||
"password": self.device.credential.password.get_secret_value(),
|
||||
"global_delay_factor": params.netmiko_delay_factor,
|
||||
"timeout": math.floor(params.request_timeout * 1.25),
|
||||
"session_timeout": math.ceil(params.request_timeout - 1),
|
||||
**global_args,
|
||||
}
|
||||
|
||||
if self.device.credential._method == "password":
|
||||
# Use password auth if no key is defined.
|
||||
driver_kwargs[
|
||||
"password"
|
||||
] = self.device.credential.password.get_secret_value()
|
||||
else:
|
||||
# Otherwise, use key auth.
|
||||
driver_kwargs["use_keys"] = True
|
||||
driver_kwargs["key_file"] = self.device.credential.key
|
||||
if self.device.credential._method == "encrypted_key":
|
||||
# If the key is encrypted, use the password field as the
|
||||
# private key password.
|
||||
driver_kwargs[
|
||||
"passphrase"
|
||||
] = self.device.credential.password.get_secret_value()
|
||||
|
||||
try:
|
||||
nm_connect_direct = ConnectHandler(**netmiko_args)
|
||||
nm_connect_direct = ConnectHandler(**driver_kwargs)
|
||||
|
||||
responses = ()
|
||||
|
||||
|
@@ -77,7 +77,6 @@ class ScrapliConnection(SSHConnection):
|
||||
"host": host or self.device._target,
|
||||
"port": port or self.device.port,
|
||||
"auth_username": self.device.credential.username,
|
||||
"auth_password": self.device.credential.password.get_secret_value(),
|
||||
"timeout_transport": math.floor(params.request_timeout * 1.25),
|
||||
"transport": "asyncssh",
|
||||
"auth_strict_key": False,
|
||||
@@ -85,6 +84,21 @@ class ScrapliConnection(SSHConnection):
|
||||
"ssh_config_file": False,
|
||||
}
|
||||
|
||||
if self.device.credential._method == "password":
|
||||
# Use password auth if no key is defined.
|
||||
driver_kwargs[
|
||||
"auth_password"
|
||||
] = self.device.credential.password.get_secret_value()
|
||||
else:
|
||||
# Otherwise, use key auth.
|
||||
driver_kwargs["auth_private_key"] = self.device.credential.key.as_posix()
|
||||
if self.device.credential._method == "encrypted_key":
|
||||
# If the key is encrypted, use the password field as the
|
||||
# private key password.
|
||||
driver_kwargs[
|
||||
"auth_private_key_passphrase"
|
||||
] = self.device.credential.password.get_secret_value()
|
||||
|
||||
driver = driver(**driver_kwargs)
|
||||
driver.logger = log.bind(logger_name=f"scrapli.driver-{driver._host}")
|
||||
|
||||
|
@@ -1,14 +1,42 @@
|
||||
"""Validate credential configuration variables."""
|
||||
|
||||
# Standard Library
|
||||
from typing import Optional
|
||||
|
||||
# Third Party
|
||||
from pydantic import SecretStr, StrictStr
|
||||
from pydantic import FilePath, SecretStr, StrictStr, constr, root_validator
|
||||
|
||||
# Local
|
||||
from ..main import HyperglassModel
|
||||
from ..main import HyperglassModelExtra
|
||||
|
||||
Methods = constr(regex=r"(password|unencrypted_key|encrypted_key)")
|
||||
|
||||
|
||||
class Credential(HyperglassModel):
|
||||
class Credential(HyperglassModelExtra):
|
||||
"""Model for per-credential config in devices.yaml."""
|
||||
|
||||
username: StrictStr
|
||||
password: SecretStr
|
||||
password: Optional[SecretStr]
|
||||
key: Optional[FilePath]
|
||||
|
||||
@root_validator
|
||||
def validate_credential(cls, values):
|
||||
"""Ensure either a password or an SSH key is set."""
|
||||
if values["key"] is None and values["password"] is None:
|
||||
raise ValueError(
|
||||
"Either a password or an SSH key must be specified for user '{}'".format(
|
||||
values["username"]
|
||||
)
|
||||
)
|
||||
return values
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Set private attribute _method based on validated model."""
|
||||
super().__init__(**kwargs)
|
||||
self._method = None
|
||||
if self.password is not None and self.key is not None:
|
||||
self._method = "encrypted_key"
|
||||
elif self.password is None:
|
||||
self._method = "unencrypted_key"
|
||||
elif self.key is None:
|
||||
self._method = "password"
|
||||
|
Reference in New Issue
Block a user