github.com/google/fleetspeak@v0.1.15-0.20240426164851-4f31f62c1aea/fleetspeak_python/fleetspeak/client_connector/connector.py (about)

     1  # Copyright 2017 Google Inc.
     2  #
     3  # Licensed under the Apache License, Version 2.0 (the "License");
     4  # you may not use this file except in compliance with the License.
     5  # You may obtain a copy of the License at
     6  #
     7  #     https://www.apache.org/licenses/LICENSE-2.0
     8  #
     9  # Unless required by applicable law or agreed to in writing, software
    10  # distributed under the License is distributed on an "AS IS" BASIS,
    11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  # See the License for the specific language governing permissions and
    13  # limitations under the License.
    14  """A client library for fleetspeak daemonservices.
    15  
    16  This library is for use by a process run by the Fleetspeak daemonservice module
    17  to send and receive messages.  The low level protocol is described in
    18  daemonservice/channel/channel.go.
    19  """
    20  
    21  import io
    22  import os
    23  import platform
    24  import struct
    25  import threading
    26  
    27  from fleetspeak.src.client.channel.proto.fleetspeak_channel import channel_pb2
    28  from fleetspeak.src.common.proto.fleetspeak import common_pb2
    29  
    30  _WINDOWS = platform.system() == "Windows"
    31  if _WINDOWS:
    32    import msvcrt  # pylint: disable=g-import-not-at-top
    33  
    34  
    35  class ProtocolError(Exception):
    36    """Raised when we do not understand the data received from Fleetspeak."""
    37  
    38  
    39  # Constants to match behavior of channel.go.
    40  _MAGIC = 0xF1EE1001
    41  
    42  # We recommend that messages be ~1MB or smaller, and daemonservice has has 2MB
    43  # hardcoded maximum.
    44  MAX_SIZE = 2 * 1024 * 1024
    45  
    46  # Format for the struct module to pack/unpack a 32 bit unsigned integer to/from
    47  # a little endian byte sequence.
    48  _STRUCT_FMT = "<I"
    49  
    50  # The number of bytes required/produced when using _STRUCT_FMT.
    51  _STRUCT_LEN = 4
    52  
    53  # Environment variables, used to find the filedescriptors left open for
    54  # us when started by Fleetspeak.
    55  _INFD_VAR = "FLEETSPEAK_COMMS_CHANNEL_INFD"
    56  _OUTFD_VAR = "FLEETSPEAK_COMMS_CHANNEL_OUTFD"
    57  
    58  
    59  def _EnvOpen(var, mode):
    60    """Open a file descriptor identified by an environment variable."""
    61    value = os.getenv(var)
    62    if value is None:
    63      raise ValueError("%s is not set" % var)
    64  
    65    fd = int(value)
    66  
    67    # If running on Windows, convert the file handle to a C file descriptor; see:
    68    # https://groups.google.com/forum/#!topic/dev-python/GeN5bFJWfJ4
    69    if _WINDOWS:
    70      fd = msvcrt.open_osfhandle(fd, 0)
    71  
    72    return io.open(fd, mode)
    73  
    74  
    75  class FleetspeakConnection(object):
    76    """A connection to the Fleetspeak system.
    77  
    78    It's safe to call methods of this class in parallel.
    79    """
    80  
    81    def __init__(self, version=None, read_file=None, write_file=None):
    82      """Connect to Fleetspeak.
    83  
    84      Connects to and begins an initial exchange of magic numbers with the
    85      Fleetspeak process. In normal use, the arguments are not required and will
    86      be created using the environment variables set by daemonservice.
    87  
    88      Args:
    89        version: A string identifying the version of the service being run. Will
    90          be included in resource reports for this service.
    91        read_file: A python file object, or similar, used to read bytes from
    92          Fleetspeak. If None, will be created based on the execution environment
    93          provided by daemonservice.
    94        write_file: A python file object, or similar, used to write bytes to
    95          Fleetspeak. If None, will be created based on the execution environment
    96          provided by daemonservice.
    97  
    98      Raises:
    99        ValueError: If read_file and write_file are not provided, and the
   100          corresponding environment variables are not set.
   101        ProtocolError: If we receive unexpected data from Fleetspeak.
   102      """
   103      self._read_file = read_file
   104      if not self._read_file:
   105        self._read_file = _EnvOpen(_INFD_VAR, "rb")
   106  
   107      self._read_lock = threading.Lock()
   108  
   109      self._write_file = write_file
   110      if not self._write_file:
   111        self._write_file = _EnvOpen(_OUTFD_VAR, "wb")
   112  
   113      self._write_lock = threading.Lock()
   114  
   115      # It is safer to send the magic number before reading it, in case the other
   116      # end does the same. Also, we'll be killed as unresponsive if we don't
   117      # write the magic number quickly enough. (Currently though, the other end is
   118      # the go implementation, which reads and writes in parallel.)
   119      self._WriteMagic()
   120  
   121      self._WriteStartupData(version)
   122      self._ReadMagic()
   123  
   124    def Send(self, message):
   125      """Send a message through Fleetspeak.
   126  
   127      Args:
   128        message: A message protocol buffer.
   129  
   130      Returns:
   131        Size of the message in bytes.
   132      Raises:
   133        ValueError: If message is not a common_pb2.Message.
   134      """
   135      if not isinstance(message, common_pb2.Message):
   136        raise ValueError("Send requires a fleetspeak.Message")
   137  
   138      if message.destination.service_name == "system":
   139        raise ValueError(
   140            "Only predefined messages can have destination.service_name =="
   141            ' "system"'
   142        )
   143  
   144      return self._SendImpl(message)
   145  
   146    def _SendImpl(self, message):
   147      if not isinstance(message, common_pb2.Message):
   148        raise ValueError("Send requires a fleetspeak.Message")
   149  
   150      buf = message.SerializeToString()
   151      if len(buf) > MAX_SIZE:
   152        raise ValueError(
   153            "Serialized message too large, size must be at most %d, got %d"
   154            % (MAX_SIZE, len(buf))
   155        )
   156  
   157      with self._write_lock:
   158        self._write_file.write(struct.pack(_STRUCT_FMT, len(buf)))
   159        self._write_file.write(buf)
   160        self._WriteMagic()
   161  
   162      return len(buf)
   163  
   164    def Recv(self):
   165      """Accept a message from Fleetspeak.
   166  
   167      Returns:
   168        A tuple (common_pb2.Message, size of the message in bytes).
   169      Raises:
   170        ProtocolError: If we receive unexpected data from Fleetspeak.
   171      """
   172      size = struct.unpack(_STRUCT_FMT, self._ReadN(_STRUCT_LEN))[0]
   173      if size > MAX_SIZE:
   174        raise ProtocolError(
   175            "Expected size to be at most %d, got %d" % (MAX_SIZE, size)
   176        )
   177      with self._read_lock:
   178        buf = self._ReadN(size)
   179        self._ReadMagic()
   180  
   181      res = common_pb2.Message()
   182      res.ParseFromString(buf)
   183  
   184      return res, len(buf)
   185  
   186    def Heartbeat(self):
   187      """Sends a heartbeat to the Fleetspeak client.
   188  
   189      If this daemonservice is configured to use heartbeats, clients that don't
   190      call this method often enough are considered faulty and are restarted by
   191      Fleetspeak.
   192      """
   193      heartbeat_msg = common_pb2.Message(
   194          message_type="Heartbeat",
   195          destination=common_pb2.Address(service_name="system"),
   196      )
   197      self._SendImpl(heartbeat_msg)
   198  
   199    def _ReadMagic(self):
   200      got = struct.unpack(_STRUCT_FMT, self._ReadN(_STRUCT_LEN))[0]
   201      if got != _MAGIC:
   202        raise ProtocolError(
   203            "Expected to read magic number {}, got {}.".format(_MAGIC, got)
   204        )
   205  
   206    def _WriteMagic(self):
   207      buf = struct.pack(_STRUCT_FMT, _MAGIC)
   208      self._write_file.write(buf)
   209      self._write_file.flush()
   210  
   211    def _WriteStartupData(self, version):
   212      startup_msg = common_pb2.Message(
   213          message_type="StartupData",
   214          destination=common_pb2.Address(service_name="system"),
   215      )
   216      startup_msg.data.Pack(
   217          channel_pb2.StartupData(pid=os.getpid(), version=version)
   218      )
   219      self._SendImpl(startup_msg)
   220  
   221    def _ReadN(self, n):
   222      """Reads n characters from the input stream, or until EOF.
   223  
   224      This is equivalent to the current CPython implementation of read(n), but
   225      not guaranteed by the docs.
   226  
   227      Args:
   228        n: int
   229  
   230      Returns:
   231        string
   232      """
   233      ret = b""
   234      while True:
   235        chunk = self._read_file.read(n - len(ret))
   236        ret += chunk
   237  
   238        if len(ret) == n or not chunk:
   239          return ret