Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IPv6 support #260

Merged
merged 1 commit into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions broker/hosts.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@ def __init__(self, **kwargs):
"""Create a Host instance.

Expected kwargs:
hostname: str - Hostname or IP address of the host, required
name: str - Name of the host
username: str - Username to use for SSH connection
password: str - Password to use for SSH connection
connection_timeout: int - Timeout for SSH connection
port: int - Port to use for SSH connection
key_filename: str - Path to SSH key file to use for SSH connection
hostname: (str) - Hostname or IP address of the host, required
name: (str) - Name of the host
username: (str) - Username to use for SSH connection
password: (str) - Password to use for SSH connection
connection_timeout: (int) - Timeout for SSH connection
port: (int) - Port to use for SSH connection
key_filename: (str) - Path to SSH key file to use for SSH connection
ipv6 (bool): Whether or not to use IPv6. Defaults to False.
ipv4_fallback (bool): Whether or not to fallback to IPv4 if IPv6 fails. Defaults to True.
Comment on lines +45 to +46
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep the type format in docstrings same across ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i can make that update

"""
logger.debug(f"Constructing host using {kwargs=}")
self.hostname = kwargs.get("hostname") or kwargs.get("ip")
Expand All @@ -59,6 +61,8 @@ def __init__(self, **kwargs):
self.timeout = kwargs.pop("connection_timeout", settings.HOST_CONNECTION_TIMEOUT)
self.port = kwargs.pop("port", settings.HOST_SSH_PORT)
self.key_filename = kwargs.pop("key_filename", settings.HOST_SSH_KEY_FILENAME)
self.ipv6 = kwargs.pop("ipv6", settings.HOST_IPV6)
self.ipv4_fallback = kwargs.pop("ipv4_fallback", settings.HOST_IPV4_FALLBACK)
self.__dict__.update(kwargs) # Make every other kwarg an attribute
self._session = None

Expand All @@ -84,7 +88,16 @@ def session(self):
self.connect()
return self._session

def connect(self, username=None, password=None, timeout=None, port=22, key_filename=None):
def connect(
self,
username=None,
password=None,
timeout=None,
port=22,
key_filename=None,
ipv6=False,
ipv4_fallback=True,
):
"""Connect to the host using SSH.

Args:
Expand All @@ -93,6 +106,8 @@ def connect(self, username=None, password=None, timeout=None, port=22, key_filen
timeout (int): The timeout for the SSH connection in seconds.
port (int): The port to use for the SSH connection. Defaults to 22.
key_filename (str): The path to the private key file to use for the SSH connection.
ipv6 (bool): Whether or not to use IPv6. Defaults to False.
ipv4_fallback (bool): Whether or not to fallback to IPv4 if IPv6 fails. Defaults to True.
"""
username = username or self.username
password = password or self.password
Expand All @@ -103,6 +118,8 @@ def connect(self, username=None, password=None, timeout=None, port=22, key_filen
if ":" in self.hostname:
_hostname, port = self.hostname.split(":")
_port = int(port)
ipv6 = ipv6 or self.ipv6
ipv4_fallback = ipv4_fallback or self.ipv4_fallback
self.close()
self._session = Session(
hostname=_hostname,
Expand All @@ -111,6 +128,8 @@ def connect(self, username=None, password=None, timeout=None, port=22, key_filen
port=_port,
key_filename=key_filename,
timeout=timeout,
ipv6=ipv6,
ipv4_fallback=ipv4_fallback,
)

def close(self):
Expand Down
64 changes: 60 additions & 4 deletions broker/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,54 @@
FILE_FLAGS = ssh2_sftp.LIBSSH2_FXF_CREAT | ssh2_sftp.LIBSSH2_FXF_WRITE


def _create_connect_socket(host, port, timeout, ipv6=False, ipv4_fallback=True, sock=None):
"""Create a socket and establish a connection to the specified host and port.

Args:
host (str): The hostname or IP address of the remote server.
port (int): The port number to connect to.
timeout (float): The timeout value in seconds for the socket connection.
ipv6 (bool, optional): Whether to use IPv6. Defaults to False.
ipv4_fallback (bool, optional): Whether to fallback to IPv4 if IPv6 fails. Defaults to True.
sock (socket.socket, optional): An existing socket object to use. Defaults to None.

Returns:
socket.socket: The connected socket object.
bool: True if IPv6 was used, False otherwise.

Raises:
exceptions.ConnectionError: If unable to establish a connection to the host.
"""
if ipv6 and not sock:
try:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
except OSError as err:
if ipv4_fallback:
logger.warning(f"IPv6 failed with {err}. Falling back to IPv4.")
return _create_connect_socket(host, port, timeout, ipv6=False)
else:
raise exceptions.ConnectionError(
f"Unable to establish IPv6 connection to {host}."
) from err
elif not sock:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
if ipv6:
try:
sock.connect((host, port))
except socket.gaierror as err:
if ipv4_fallback:
logger.warning(f"IPv6 connection failed to {host}. Falling back to IPv4.")
return _create_connect_socket(host, port, timeout, ipv6=False, sock=sock)
else:
raise exceptions.ConnectionError(
f"Unable to establish IPv6 connection to {host}."
) from err
else:
sock.connect((host, port))
return sock, ipv6


class Session:
"""Wrapper around ssh2-python's auth/connection system."""

Expand All @@ -43,22 +91,30 @@ def __init__(self, **kwargs):
port (int): The port number to connect to. Defaults to 22.
key_filename (str): The path to the private key file to use for authentication.
password (str): The password to use for authentication.
ipv6 (bool): Whether or not to use IPv6. Defaults to False.
ipv4_fallback (bool): Whether or not to fallback to IPv4 if IPv6 fails. Defaults to True.

Raises:
AuthException: If no password or key file is provided.
ConnectionError: If the connection fails.
FileNotFoundError: If the key file is not found.
"""
host = kwargs.get("hostname", "localhost")
user = kwargs.get("username", "root")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(kwargs.get("timeout"))
port = kwargs.get("port", 22)
key_filename = kwargs.get("key_filename")
password = kwargs.get("password")
timeout = kwargs.get("timeout", 60)
helpers.simple_retry(sock.connect, [(host, port)], max_timeout=timeout)
# create the socket
self.sock, self.is_ipv6 = _create_connect_socket(
host,
port,
timeout,
ipv6=kwargs.get("ipv6", False),
ipv4_fallback=kwargs.get("ipv4_fallback", True),
)
self.session = ssh2_Session()
self.session.handshake(sock)
self.session.handshake(self.sock)
try:
if key_filename:
auth_type = "Key"
Expand Down
2 changes: 2 additions & 0 deletions broker/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ def init_settings(settings_path, interactive=False):
Validator("HOST_CONNECTION_TIMEOUT", default=60),
Validator("HOST_SSH_PORT", default=22),
Validator("HOST_SSH_KEY_FILENAME", default=None),
Validator("HOST_IPV6", default=False),
Validator("HOST_IPV4_FALLBACK", default=True),
Validator("LOGGING", is_type_of=dict),
Validator(
"LOGGING.CONSOLE_LEVEL",
Expand Down
4 changes: 4 additions & 0 deletions broker_settings.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ host_username: root
host_password: "<password>"
host_ssh_port: 22
host_ssh_key_filename: "</path/to/the/ssh-key>"
# Default all host ssh connections to IPv6
host_ipv6: False
# If IPv6 connection attempts fail, fallback to IPv4
host_ipv4_fallback: True
# Provider settings
AnsibleTower:
base_url: "https://<ansible tower host>/"
Expand Down
Loading