132 lines
4.5 KiB
Python
132 lines
4.5 KiB
Python
import os
|
|
import socket
|
|
import stat
|
|
import struct
|
|
import time
|
|
|
|
from host_agent_protocol import (
|
|
CLIENT_CONNECT_TIMEOUT_SECONDS,
|
|
EXCHANGE_TIMEOUT_SECONDS,
|
|
HOST_AGENT_SOCKET_PATH,
|
|
MAX_RESPONSE_PAYLOAD_BYTES,
|
|
HostAgentAction,
|
|
HostAgentProtocolError,
|
|
HostAgentRequest,
|
|
HostAgentStatus,
|
|
decode_response_frame,
|
|
encode_request_frame,
|
|
receive_frame,
|
|
require_eof,
|
|
send_frame,
|
|
)
|
|
|
|
|
|
class HostAgentClientError(RuntimeError):
|
|
def __init__(self, category):
|
|
self.category = category
|
|
super().__init__('host operations agent request failed')
|
|
|
|
|
|
class HostAgentUnavailableError(HostAgentClientError):
|
|
pass
|
|
|
|
|
|
class HostAgentRejectedError(HostAgentClientError):
|
|
pass
|
|
|
|
|
|
def _fixed_socket_is_safe():
|
|
try:
|
|
details = os.lstat(HOST_AGENT_SOCKET_PATH)
|
|
except OSError:
|
|
return False
|
|
return stat.S_ISSOCK(details.st_mode) and details.st_uid == 0
|
|
|
|
|
|
def _peer_credentials(sock):
|
|
if not hasattr(socket, 'SO_PEERCRED'):
|
|
raise HostAgentUnavailableError('peer_credentials_unavailable')
|
|
try:
|
|
raw = sock.getsockopt(
|
|
socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize('3i'),
|
|
)
|
|
pid, uid, gid = struct.unpack('3i', raw)
|
|
except (OSError, struct.error) as exc:
|
|
raise HostAgentUnavailableError('peer_credentials_unavailable') from exc
|
|
# A peer outside the client's PID namespace is reported as PID 0 even
|
|
# though its UID/GID remain authoritative through SO_PEERCRED.
|
|
if pid < 0:
|
|
raise HostAgentUnavailableError('peer_identity_invalid')
|
|
return pid, uid, gid
|
|
|
|
|
|
class HostAgentClient:
|
|
def __init__(self):
|
|
pass
|
|
|
|
@staticmethod
|
|
def is_available():
|
|
return _fixed_socket_is_safe()
|
|
|
|
def dispatch(
|
|
self, *, operation_id, action, active_config_sha256,
|
|
active_secrets_sha256, candidate_config_sha256,
|
|
candidate_secrets_sha256,
|
|
):
|
|
try:
|
|
request = HostAgentRequest(
|
|
operation_id=operation_id,
|
|
action=HostAgentAction(action),
|
|
active_config_sha256=active_config_sha256,
|
|
active_secrets_sha256=active_secrets_sha256,
|
|
candidate_config_sha256=candidate_config_sha256,
|
|
candidate_secrets_sha256=candidate_secrets_sha256,
|
|
)
|
|
frame = encode_request_frame(request)
|
|
except (HostAgentProtocolError, TypeError, ValueError) as exc:
|
|
raise HostAgentRejectedError('request_invalid') from exc
|
|
if not _fixed_socket_is_safe():
|
|
raise HostAgentUnavailableError('socket_unavailable')
|
|
deadline = time.monotonic() + EXCHANGE_TIMEOUT_SECONDS
|
|
connection = None
|
|
response = None
|
|
try:
|
|
family = getattr(socket, 'AF_UNIX', None)
|
|
if family is None:
|
|
raise HostAgentUnavailableError('unix_socket_unavailable')
|
|
connection = socket.socket(family, socket.SOCK_STREAM)
|
|
connection.settimeout(min(
|
|
CLIENT_CONNECT_TIMEOUT_SECONDS,
|
|
max(0.001, deadline - time.monotonic()),
|
|
))
|
|
connection.connect(HOST_AGENT_SOCKET_PATH)
|
|
_pid, peer_uid, _gid = _peer_credentials(connection)
|
|
if peer_uid != 0:
|
|
raise HostAgentUnavailableError('server_identity_invalid')
|
|
send_frame(connection, frame, deadline=deadline)
|
|
connection.shutdown(socket.SHUT_WR)
|
|
response = decode_response_frame(receive_frame(
|
|
connection, maximum=MAX_RESPONSE_PAYLOAD_BYTES, deadline=deadline,
|
|
))
|
|
require_eof(connection, deadline=deadline)
|
|
except HostAgentClientError:
|
|
raise
|
|
except HostAgentProtocolError as exc:
|
|
raise HostAgentUnavailableError('response_invalid') from exc
|
|
except (OSError, TimeoutError) as exc:
|
|
raise HostAgentUnavailableError('transport_unavailable') from exc
|
|
finally:
|
|
if connection is not None:
|
|
try:
|
|
connection.close()
|
|
except OSError:
|
|
pass
|
|
connection = frame = request = family = None
|
|
if response.operation_id != operation_id:
|
|
raise HostAgentUnavailableError('response_identity_invalid')
|
|
if response.status is HostAgentStatus.ACCEPTED:
|
|
return response
|
|
if response.status is HostAgentStatus.REJECTED:
|
|
raise HostAgentRejectedError('request_rejected')
|
|
raise HostAgentUnavailableError('request_unavailable')
|