openrvdas.logger.writers.udp_writer

No module-level documentation available.
  1#!/usr/bin/env python3
  2
  3import errno
  4import logging
  5import socket
  6import struct
  7
  8from typing import Union
  9
 10from logger.utils.das_record import DASRecord  # noqa E402
 11from logger.writers.writer import Writer  # noqa E402
 12
 13# So that we can write the user's record no matter how silly big it is, we
 14# autodetect the system's maximum datagram size and write() fragments the
 15# record into smaller packets if needed.  Each fragmented packet is marked with
 16# FRAGMENT_MARKER so that UDPReader can notice and reassemble the record.
 17FRAGMENT_MARKER = b'\xff\xffTOOBIG\xff\xff'
 18
 19# Maximum allowable size of a UDP datagram on this system, autodetect once at
 20# module load
 21#
 22# NOTE: You can test/debug the fragmentation code by loading the udp_writer
 23#       module and than manually setting udp_writer.MAXSIZE after autodetection
 24#       has happened.
 25#
 26MAXSIZE = None
 27
 28
 29def __detect_maxsize():
 30    """Autodetect the maximum datagram size we can send"""
 31    global MAXSIZE
 32    if MAXSIZE:
 33        return
 34
 35    s = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM,
 36                      proto=socket.IPPROTO_UDP)
 37
 38    # start with the maximum allowable size of a UDP packet, and work our way
 39    # down one byte at a time.
 40    #
 41    # FIXME: I know, I know.  Lame, slow, etc, a binary search would be "most
 42    #        correct", but I don't feel like making this go faster right now.
 43    #        And it's only done once, so whatever.
 44    #
 45    trysize = 65535
 46    while trysize > 0:
 47        logging.debug("__detect_maxsize: trying %d", trysize)
 48        try:
 49            s.sendto(b'a'*trysize, ('127.0.0.1', 9999))
 50        except OSError as e:
 51            if e.errno == errno.EMSGSIZE:
 52                trysize -= 1
 53                continue
 54            else:
 55                # For whatever reason, this won't work... print a warning
 56                logging.warning("__detect_maxsize: send() failed: %s", str(e))
 57                break
 58        # outstanding!
 59        break
 60
 61    if not trysize:
 62        logging.warning("Failed to autodetect maximum UDP datagram size. "
 63                        "Record fragmentation disabled")
 64    else:
 65        logging.info("Detected maximum UDP datagram size %d", trysize)
 66        MAXSIZE = trysize
 67
 68
 69# attempt to detect maximum datagram size when module is loaded
 70__detect_maxsize()
 71
 72
 73class UDPWriter(Writer):
 74    """Write UDP packets to network."""
 75
 76    def __init__(self, destination=None, port=None,
 77                 mc_interface=None, mc_ttl=3, num_retry=2, warning_limit=5, eol='',
 78                 reuseaddr=False, reuseport=False, **kwargs):
 79        """Write records to a UDP network socket.
 80        ```
 81        destination  The destination to send UDP packets to. If '' or None,
 82                     the UDPWriter will broadcast to 255.255.255.255.  On a
 83                     system connected to more than one subnet, you'll want to
 84                     specify the broadcast address of the network you're trying
 85                     to send to (e.g., 192.168.1.255).
 86
 87        port         Port to which packets should be sent.  REQUIRED
 88
 89        mc_interface REQUIRED for multicast, the interface to send from.  Can be
 90                     specified as either IP or a resolvable hostname.
 91
 92        mc_ttl       For multicast, how many network hops to allow.
 93
 94        num_retry    Number of times to retry if write fails. If writer exceeds
 95                     this number, it will give up on writing the message and
 96                     move on.
 97
 98        warning_limit  Number of times the writer gives up on writing a message
 99                     (without any intervening successes) before it gives up complaining
100                     about failures.
101
102        eol          If specified, an end of line string to append to record
103                     before sending.
104
105        reuseaddr    Specifies wether to set SO_REUSEADDR on the created socket.  If
106                     you don't know you need this, don't enable it.
107
108        reuseport    Specifies wether to set SO_REUSEPORT on the created socket.  If
109                     you don't know you need this, don't enable it.
110
111        encoding - 'utf-8' by default. If empty or None, do not attempt any
112                decoding and return raw bytes. Other possible encodings are
113                listed in online documentation here:
114                https://docs.python.org/3/library/codecs.html#standard-encodings
115
116        encoding_errors - 'ignore' by default. Other error strategies are
117                'strict', 'replace', and 'backslashreplace', described here:
118                https://docs.python.org/3/howto/unicode.html#encodings
119        ```
120
121        """
122        # Initialize type checking
123        super().__init__(**kwargs)  # processes 'quiet', encodings and type hints
124
125        self.num_retry = num_retry
126        self.warning_limit = warning_limit
127        self.num_warnings = 0
128        self.good_writes = 0  # consecutive good writes, for detecting UDP errors
129
130        # 'eol' comes in as a (probably escaped) string. We need to
131        # unescape it, which means converting to bytes and back.
132        if eol is not None and self.encoding:
133            eol = self._unescape_str(eol)
134        self.eol = eol
135
136        self.target_str = 'destination: %s, port: %d' % (destination, port)
137
138        # do name resolution once in the constructor
139        #
140        # NOTE: This means the hostname must be valid when we start, otherwise
141        #       the config_check code will puke.  That's fine.  The alternative
142        #       is we let name resolution happen while we're running, but then
143        #       each failed lookup is going to block our write() routine for a
144        #       few seconds - not good.
145        #
146        # NOTE: This also catches specifying impropperly formatted IP
147        #       addresses.  The only way through gethostbyname() w/out throwing
148        #       an exception is to provide a valid hostname or IP address.
149        #       Propperly formatted IPs just get returned.
150        #
151        if destination:
152            destination = socket.gethostbyname(destination)
153        else:
154            # If no destination, it's a broadcast; set dest to special string
155            destination = '<broadcast>'
156
157        self.destination = destination
158
159        # make sure user passed in `port`
160        #
161        # NOTE: We want the order of the arguments to consistently be (ip,
162        #       port, ...) across all the network readers/writers... but we
163        #       want `destination` to be optional.  All kwargs need to come
164        #       after all regular args, so we've assigned a default value of
165        #       None to `port`.  But don't be confused, it is REQUIRED.
166        #
167        if not port:
168            raise TypeError('must specify `port`')
169        # make sure port gets stored as an int, even if passed in as a string
170        self.port = int(port)
171
172        # multicast options
173        if mc_interface:
174            # resolve once in constructor
175            mc_interface = socket.gethostbyname(mc_interface)
176        self.mc_interface = mc_interface
177        self.mc_ttl = mc_ttl
178
179        self.reuseaddr = reuseaddr
180        self.reuseport = reuseport
181
182        # socket gets initialized on-demand in write()
183        self.socket = None
184
185    ############################
186    def __del__(self):
187        if self.socket:
188            self.socket.close()
189
190    ############################
191    def _open_socket(self):
192        """Do socket prep so we're ready to write().  Returns socket object or None on
193        failure.
194        """
195        udp_socket = socket.socket(family=socket.AF_INET,
196                                   type=socket.SOCK_DGRAM,
197                                   proto=socket.IPPROTO_UDP)
198        if self.reuseaddr:
199            udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
200        if self.reuseport:
201            try:  # Raspbian doesn't recognize SO_REUSEPORT
202                udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, True)
203            except AttributeError:
204                logging.warning('Unable to set socket REUSEPORT; may be unsupported')
205
206        # set multicast/broadcast options
207        if self.mc_interface:
208            # set the time-to-live for messages
209            udp_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL,
210                                  struct.pack('b', self.mc_ttl))
211            # set outgoing multicast interface
212            udp_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF,
213                                  socket.inet_aton(self.mc_interface))
214        else:
215            # maybe broadcast, but very non-trivial to detect broadcast IP, so
216            # we set the broadcast flag anytime we're not doing multicast
217            udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, True)
218
219        try:
220            udp_socket.connect((self.destination, self.port))
221            return udp_socket
222        except OSError as e:
223            logging.error('Unable to connect to %s:%d - %s', self.destination, self.port, e)
224            return None
225
226    ############################
227    def write(self, record: Union[str, bytes, DASRecord]):
228        """Write the record to the network."""
229
230        # See if it's something we can process, and if not, try digesting
231        if not self.can_process_record(record):  # inherited from BaseModule()
232            self.digest_record(record)  # inherited from BaseModule()
233            return
234
235        if isinstance(record, DASRecord):
236            record = record.as_json()
237
238        # Append eol if configured
239        if self.eol:
240            record += self.eol
241
242        # Encode the record, so we're dealing with bytes from here on out
243        record = self._encode_str(record)
244
245        # Fragment record if needed, and recurse.
246        if len(record) > MAXSIZE:
247            record_list = []
248            max_fragment_size = MAXSIZE - len(FRAGMENT_MARKER)
249            fragment_sizes = []
250            while len(record) > max_fragment_size:
251                r = record[:max_fragment_size]+FRAGMENT_MARKER
252                record_list.append(r)
253                fragment_sizes.append(str(len(r)))
254                record = record[max_fragment_size:]
255            # last record doesn't get FRAGMENT_MARKER
256            record_list.append(record)
257            fragment_sizes.append("{} bytes".format(len(record)))
258            fragment_sizes = ', '.join(fragment_sizes)
259            logging.info("write: fragmented record into %d datagrams: %s",
260                         len(record_list), fragment_sizes)
261            logging.debug(str(record_list))
262
263            # change our encoding to binary temporarily, because we've already
264            # encoded to binary and added our marker (which has non-utf chars
265            # in it)
266            old_encoding = self.encoding
267            self.encoding = None
268            self.write(record_list)
269            # restore old encoding
270            self.encoding = old_encoding
271            return
272
273        # If socket isn't connected, try reconnecting. If we can't
274        # reconnect, complain and return without writing.
275        if not self.socket:
276            self.socket = self._open_socket()
277        if not self.socket:
278            logging.error('Unable to write record to %s:%d',
279                          self.destination, self.port)
280            return
281
282        num_tries = bytes_sent = 0
283        rec_len = len(record)
284        while num_tries <= self.num_retry and bytes_sent < rec_len:
285            try:
286                bytes_sent = self.socket.send(record)
287
288                # If here, write succeeded. Reset warnings
289                #
290                # NOTE: If the host is unreachable, every other send will fail.
291                #       Since UDP doesn't actually know it failed, the initial
292                #       send() cannot fail.  However, the network stack will
293                #       see the ICMP host unreachable message and will store
294                #       THAT as the the error message for next write, then the
295                #       next send fails and clears the error...  Then the next
296                #       "succeeds" and the next fails, etc, etc
297                #
298                #       So we look for 2 consecutive "successful" writes before
299                #       resetting num_warnings.
300                #
301                self.good_writes += 1
302                if self.good_writes >= 2:
303                    if self.num_warnings == self.warning_limit:
304                        logging.info('UDPWriter.write() succeeded in writing after series of '
305                                     'failures; resetting warnings.')
306                    self.num_warnings = 0  # we've succeeded
307
308            except (OSError, ConnectionRefusedError) as e:
309                # If we failed, complain, unless we've already complained too much
310                self.good_writes = 0
311                if self.num_warnings < self.warning_limit:
312                    logging.error(f'UDPWriter: send() error: {self.target_str}: {str(e)}')
313                    if 'Message too long' in str(e):
314                        logging.error(f'Message length is {rec_len}')
315                    self.num_warnings += 1
316                    if self.num_warnings == self.warning_limit:
317                        logging.error('UDPWriter.write() - muting errors')
318            num_tries += 1
319
320        logging.debug('UDPWriter.write() wrote %d/%d bytes after %d tries',
321                      bytes_sent, rec_len, num_tries)
FRAGMENT_MARKER = b'\xff\xffTOOBIG\xff\xff'
MAXSIZE = 65507
class UDPWriter(logger.writers.writer.Writer):
 74class UDPWriter(Writer):
 75    """Write UDP packets to network."""
 76
 77    def __init__(self, destination=None, port=None,
 78                 mc_interface=None, mc_ttl=3, num_retry=2, warning_limit=5, eol='',
 79                 reuseaddr=False, reuseport=False, **kwargs):
 80        """Write records to a UDP network socket.
 81        ```
 82        destination  The destination to send UDP packets to. If '' or None,
 83                     the UDPWriter will broadcast to 255.255.255.255.  On a
 84                     system connected to more than one subnet, you'll want to
 85                     specify the broadcast address of the network you're trying
 86                     to send to (e.g., 192.168.1.255).
 87
 88        port         Port to which packets should be sent.  REQUIRED
 89
 90        mc_interface REQUIRED for multicast, the interface to send from.  Can be
 91                     specified as either IP or a resolvable hostname.
 92
 93        mc_ttl       For multicast, how many network hops to allow.
 94
 95        num_retry    Number of times to retry if write fails. If writer exceeds
 96                     this number, it will give up on writing the message and
 97                     move on.
 98
 99        warning_limit  Number of times the writer gives up on writing a message
100                     (without any intervening successes) before it gives up complaining
101                     about failures.
102
103        eol          If specified, an end of line string to append to record
104                     before sending.
105
106        reuseaddr    Specifies wether to set SO_REUSEADDR on the created socket.  If
107                     you don't know you need this, don't enable it.
108
109        reuseport    Specifies wether to set SO_REUSEPORT on the created socket.  If
110                     you don't know you need this, don't enable it.
111
112        encoding - 'utf-8' by default. If empty or None, do not attempt any
113                decoding and return raw bytes. Other possible encodings are
114                listed in online documentation here:
115                https://docs.python.org/3/library/codecs.html#standard-encodings
116
117        encoding_errors - 'ignore' by default. Other error strategies are
118                'strict', 'replace', and 'backslashreplace', described here:
119                https://docs.python.org/3/howto/unicode.html#encodings
120        ```
121
122        """
123        # Initialize type checking
124        super().__init__(**kwargs)  # processes 'quiet', encodings and type hints
125
126        self.num_retry = num_retry
127        self.warning_limit = warning_limit
128        self.num_warnings = 0
129        self.good_writes = 0  # consecutive good writes, for detecting UDP errors
130
131        # 'eol' comes in as a (probably escaped) string. We need to
132        # unescape it, which means converting to bytes and back.
133        if eol is not None and self.encoding:
134            eol = self._unescape_str(eol)
135        self.eol = eol
136
137        self.target_str = 'destination: %s, port: %d' % (destination, port)
138
139        # do name resolution once in the constructor
140        #
141        # NOTE: This means the hostname must be valid when we start, otherwise
142        #       the config_check code will puke.  That's fine.  The alternative
143        #       is we let name resolution happen while we're running, but then
144        #       each failed lookup is going to block our write() routine for a
145        #       few seconds - not good.
146        #
147        # NOTE: This also catches specifying impropperly formatted IP
148        #       addresses.  The only way through gethostbyname() w/out throwing
149        #       an exception is to provide a valid hostname or IP address.
150        #       Propperly formatted IPs just get returned.
151        #
152        if destination:
153            destination = socket.gethostbyname(destination)
154        else:
155            # If no destination, it's a broadcast; set dest to special string
156            destination = '<broadcast>'
157
158        self.destination = destination
159
160        # make sure user passed in `port`
161        #
162        # NOTE: We want the order of the arguments to consistently be (ip,
163        #       port, ...) across all the network readers/writers... but we
164        #       want `destination` to be optional.  All kwargs need to come
165        #       after all regular args, so we've assigned a default value of
166        #       None to `port`.  But don't be confused, it is REQUIRED.
167        #
168        if not port:
169            raise TypeError('must specify `port`')
170        # make sure port gets stored as an int, even if passed in as a string
171        self.port = int(port)
172
173        # multicast options
174        if mc_interface:
175            # resolve once in constructor
176            mc_interface = socket.gethostbyname(mc_interface)
177        self.mc_interface = mc_interface
178        self.mc_ttl = mc_ttl
179
180        self.reuseaddr = reuseaddr
181        self.reuseport = reuseport
182
183        # socket gets initialized on-demand in write()
184        self.socket = None
185
186    ############################
187    def __del__(self):
188        if self.socket:
189            self.socket.close()
190
191    ############################
192    def _open_socket(self):
193        """Do socket prep so we're ready to write().  Returns socket object or None on
194        failure.
195        """
196        udp_socket = socket.socket(family=socket.AF_INET,
197                                   type=socket.SOCK_DGRAM,
198                                   proto=socket.IPPROTO_UDP)
199        if self.reuseaddr:
200            udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
201        if self.reuseport:
202            try:  # Raspbian doesn't recognize SO_REUSEPORT
203                udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, True)
204            except AttributeError:
205                logging.warning('Unable to set socket REUSEPORT; may be unsupported')
206
207        # set multicast/broadcast options
208        if self.mc_interface:
209            # set the time-to-live for messages
210            udp_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL,
211                                  struct.pack('b', self.mc_ttl))
212            # set outgoing multicast interface
213            udp_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF,
214                                  socket.inet_aton(self.mc_interface))
215        else:
216            # maybe broadcast, but very non-trivial to detect broadcast IP, so
217            # we set the broadcast flag anytime we're not doing multicast
218            udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, True)
219
220        try:
221            udp_socket.connect((self.destination, self.port))
222            return udp_socket
223        except OSError as e:
224            logging.error('Unable to connect to %s:%d - %s', self.destination, self.port, e)
225            return None
226
227    ############################
228    def write(self, record: Union[str, bytes, DASRecord]):
229        """Write the record to the network."""
230
231        # See if it's something we can process, and if not, try digesting
232        if not self.can_process_record(record):  # inherited from BaseModule()
233            self.digest_record(record)  # inherited from BaseModule()
234            return
235
236        if isinstance(record, DASRecord):
237            record = record.as_json()
238
239        # Append eol if configured
240        if self.eol:
241            record += self.eol
242
243        # Encode the record, so we're dealing with bytes from here on out
244        record = self._encode_str(record)
245
246        # Fragment record if needed, and recurse.
247        if len(record) > MAXSIZE:
248            record_list = []
249            max_fragment_size = MAXSIZE - len(FRAGMENT_MARKER)
250            fragment_sizes = []
251            while len(record) > max_fragment_size:
252                r = record[:max_fragment_size]+FRAGMENT_MARKER
253                record_list.append(r)
254                fragment_sizes.append(str(len(r)))
255                record = record[max_fragment_size:]
256            # last record doesn't get FRAGMENT_MARKER
257            record_list.append(record)
258            fragment_sizes.append("{} bytes".format(len(record)))
259            fragment_sizes = ', '.join(fragment_sizes)
260            logging.info("write: fragmented record into %d datagrams: %s",
261                         len(record_list), fragment_sizes)
262            logging.debug(str(record_list))
263
264            # change our encoding to binary temporarily, because we've already
265            # encoded to binary and added our marker (which has non-utf chars
266            # in it)
267            old_encoding = self.encoding
268            self.encoding = None
269            self.write(record_list)
270            # restore old encoding
271            self.encoding = old_encoding
272            return
273
274        # If socket isn't connected, try reconnecting. If we can't
275        # reconnect, complain and return without writing.
276        if not self.socket:
277            self.socket = self._open_socket()
278        if not self.socket:
279            logging.error('Unable to write record to %s:%d',
280                          self.destination, self.port)
281            return
282
283        num_tries = bytes_sent = 0
284        rec_len = len(record)
285        while num_tries <= self.num_retry and bytes_sent < rec_len:
286            try:
287                bytes_sent = self.socket.send(record)
288
289                # If here, write succeeded. Reset warnings
290                #
291                # NOTE: If the host is unreachable, every other send will fail.
292                #       Since UDP doesn't actually know it failed, the initial
293                #       send() cannot fail.  However, the network stack will
294                #       see the ICMP host unreachable message and will store
295                #       THAT as the the error message for next write, then the
296                #       next send fails and clears the error...  Then the next
297                #       "succeeds" and the next fails, etc, etc
298                #
299                #       So we look for 2 consecutive "successful" writes before
300                #       resetting num_warnings.
301                #
302                self.good_writes += 1
303                if self.good_writes >= 2:
304                    if self.num_warnings == self.warning_limit:
305                        logging.info('UDPWriter.write() succeeded in writing after series of '
306                                     'failures; resetting warnings.')
307                    self.num_warnings = 0  # we've succeeded
308
309            except (OSError, ConnectionRefusedError) as e:
310                # If we failed, complain, unless we've already complained too much
311                self.good_writes = 0
312                if self.num_warnings < self.warning_limit:
313                    logging.error(f'UDPWriter: send() error: {self.target_str}: {str(e)}')
314                    if 'Message too long' in str(e):
315                        logging.error(f'Message length is {rec_len}')
316                    self.num_warnings += 1
317                    if self.num_warnings == self.warning_limit:
318                        logging.error('UDPWriter.write() - muting errors')
319            num_tries += 1
320
321        logging.debug('UDPWriter.write() wrote %d/%d bytes after %d tries',
322                      bytes_sent, rec_len, num_tries)

Write UDP packets to network.

UDPWriter( destination=None, port=None, mc_interface=None, mc_ttl=3, num_retry=2, warning_limit=5, eol='', reuseaddr=False, reuseport=False, **kwargs)
 77    def __init__(self, destination=None, port=None,
 78                 mc_interface=None, mc_ttl=3, num_retry=2, warning_limit=5, eol='',
 79                 reuseaddr=False, reuseport=False, **kwargs):
 80        """Write records to a UDP network socket.
 81        ```
 82        destination  The destination to send UDP packets to. If '' or None,
 83                     the UDPWriter will broadcast to 255.255.255.255.  On a
 84                     system connected to more than one subnet, you'll want to
 85                     specify the broadcast address of the network you're trying
 86                     to send to (e.g., 192.168.1.255).
 87
 88        port         Port to which packets should be sent.  REQUIRED
 89
 90        mc_interface REQUIRED for multicast, the interface to send from.  Can be
 91                     specified as either IP or a resolvable hostname.
 92
 93        mc_ttl       For multicast, how many network hops to allow.
 94
 95        num_retry    Number of times to retry if write fails. If writer exceeds
 96                     this number, it will give up on writing the message and
 97                     move on.
 98
 99        warning_limit  Number of times the writer gives up on writing a message
100                     (without any intervening successes) before it gives up complaining
101                     about failures.
102
103        eol          If specified, an end of line string to append to record
104                     before sending.
105
106        reuseaddr    Specifies wether to set SO_REUSEADDR on the created socket.  If
107                     you don't know you need this, don't enable it.
108
109        reuseport    Specifies wether to set SO_REUSEPORT on the created socket.  If
110                     you don't know you need this, don't enable it.
111
112        encoding - 'utf-8' by default. If empty or None, do not attempt any
113                decoding and return raw bytes. Other possible encodings are
114                listed in online documentation here:
115                https://docs.python.org/3/library/codecs.html#standard-encodings
116
117        encoding_errors - 'ignore' by default. Other error strategies are
118                'strict', 'replace', and 'backslashreplace', described here:
119                https://docs.python.org/3/howto/unicode.html#encodings
120        ```
121
122        """
123        # Initialize type checking
124        super().__init__(**kwargs)  # processes 'quiet', encodings and type hints
125
126        self.num_retry = num_retry
127        self.warning_limit = warning_limit
128        self.num_warnings = 0
129        self.good_writes = 0  # consecutive good writes, for detecting UDP errors
130
131        # 'eol' comes in as a (probably escaped) string. We need to
132        # unescape it, which means converting to bytes and back.
133        if eol is not None and self.encoding:
134            eol = self._unescape_str(eol)
135        self.eol = eol
136
137        self.target_str = 'destination: %s, port: %d' % (destination, port)
138
139        # do name resolution once in the constructor
140        #
141        # NOTE: This means the hostname must be valid when we start, otherwise
142        #       the config_check code will puke.  That's fine.  The alternative
143        #       is we let name resolution happen while we're running, but then
144        #       each failed lookup is going to block our write() routine for a
145        #       few seconds - not good.
146        #
147        # NOTE: This also catches specifying impropperly formatted IP
148        #       addresses.  The only way through gethostbyname() w/out throwing
149        #       an exception is to provide a valid hostname or IP address.
150        #       Propperly formatted IPs just get returned.
151        #
152        if destination:
153            destination = socket.gethostbyname(destination)
154        else:
155            # If no destination, it's a broadcast; set dest to special string
156            destination = '<broadcast>'
157
158        self.destination = destination
159
160        # make sure user passed in `port`
161        #
162        # NOTE: We want the order of the arguments to consistently be (ip,
163        #       port, ...) across all the network readers/writers... but we
164        #       want `destination` to be optional.  All kwargs need to come
165        #       after all regular args, so we've assigned a default value of
166        #       None to `port`.  But don't be confused, it is REQUIRED.
167        #
168        if not port:
169            raise TypeError('must specify `port`')
170        # make sure port gets stored as an int, even if passed in as a string
171        self.port = int(port)
172
173        # multicast options
174        if mc_interface:
175            # resolve once in constructor
176            mc_interface = socket.gethostbyname(mc_interface)
177        self.mc_interface = mc_interface
178        self.mc_ttl = mc_ttl
179
180        self.reuseaddr = reuseaddr
181        self.reuseport = reuseport
182
183        # socket gets initialized on-demand in write()
184        self.socket = None

Write records to a UDP network socket.

destination  The destination to send UDP packets to. If '' or None,
             the UDPWriter will broadcast to 255.255.255.255.  On a
             system connected to more than one subnet, you'll want to
             specify the broadcast address of the network you're trying
             to send to (e.g., 192.168.1.255).

port         Port to which packets should be sent.  REQUIRED

mc_interface REQUIRED for multicast, the interface to send from.  Can be
             specified as either IP or a resolvable hostname.

mc_ttl       For multicast, how many network hops to allow.

num_retry    Number of times to retry if write fails. If writer exceeds
             this number, it will give up on writing the message and
             move on.

warning_limit  Number of times the writer gives up on writing a message
             (without any intervening successes) before it gives up complaining
             about failures.

eol          If specified, an end of line string to append to record
             before sending.

reuseaddr    Specifies wether to set SO_REUSEADDR on the created socket.  If
             you don't know you need this, don't enable it.

reuseport    Specifies wether to set SO_REUSEPORT on the created socket.  If
             you don't know you need this, don't enable it.

encoding - 'utf-8' by default. If empty or None, do not attempt any
        decoding and return raw bytes. Other possible encodings are
        listed in online documentation here:
        https://docs.python.org/3/library/codecs.html#standard-encodings

encoding_errors - 'ignore' by default. Other error strategies are
        'strict', 'replace', and 'backslashreplace', described here:
        https://docs.python.org/3/howto/unicode.html#encodings
num_retry
warning_limit
num_warnings
good_writes
eol
target_str
destination
port
mc_interface
mc_ttl
reuseaddr
reuseport
socket
def write(self, record: Union[str, bytes, logger.utils.das_record.DASRecord]):
228    def write(self, record: Union[str, bytes, DASRecord]):
229        """Write the record to the network."""
230
231        # See if it's something we can process, and if not, try digesting
232        if not self.can_process_record(record):  # inherited from BaseModule()
233            self.digest_record(record)  # inherited from BaseModule()
234            return
235
236        if isinstance(record, DASRecord):
237            record = record.as_json()
238
239        # Append eol if configured
240        if self.eol:
241            record += self.eol
242
243        # Encode the record, so we're dealing with bytes from here on out
244        record = self._encode_str(record)
245
246        # Fragment record if needed, and recurse.
247        if len(record) > MAXSIZE:
248            record_list = []
249            max_fragment_size = MAXSIZE - len(FRAGMENT_MARKER)
250            fragment_sizes = []
251            while len(record) > max_fragment_size:
252                r = record[:max_fragment_size]+FRAGMENT_MARKER
253                record_list.append(r)
254                fragment_sizes.append(str(len(r)))
255                record = record[max_fragment_size:]
256            # last record doesn't get FRAGMENT_MARKER
257            record_list.append(record)
258            fragment_sizes.append("{} bytes".format(len(record)))
259            fragment_sizes = ', '.join(fragment_sizes)
260            logging.info("write: fragmented record into %d datagrams: %s",
261                         len(record_list), fragment_sizes)
262            logging.debug(str(record_list))
263
264            # change our encoding to binary temporarily, because we've already
265            # encoded to binary and added our marker (which has non-utf chars
266            # in it)
267            old_encoding = self.encoding
268            self.encoding = None
269            self.write(record_list)
270            # restore old encoding
271            self.encoding = old_encoding
272            return
273
274        # If socket isn't connected, try reconnecting. If we can't
275        # reconnect, complain and return without writing.
276        if not self.socket:
277            self.socket = self._open_socket()
278        if not self.socket:
279            logging.error('Unable to write record to %s:%d',
280                          self.destination, self.port)
281            return
282
283        num_tries = bytes_sent = 0
284        rec_len = len(record)
285        while num_tries <= self.num_retry and bytes_sent < rec_len:
286            try:
287                bytes_sent = self.socket.send(record)
288
289                # If here, write succeeded. Reset warnings
290                #
291                # NOTE: If the host is unreachable, every other send will fail.
292                #       Since UDP doesn't actually know it failed, the initial
293                #       send() cannot fail.  However, the network stack will
294                #       see the ICMP host unreachable message and will store
295                #       THAT as the the error message for next write, then the
296                #       next send fails and clears the error...  Then the next
297                #       "succeeds" and the next fails, etc, etc
298                #
299                #       So we look for 2 consecutive "successful" writes before
300                #       resetting num_warnings.
301                #
302                self.good_writes += 1
303                if self.good_writes >= 2:
304                    if self.num_warnings == self.warning_limit:
305                        logging.info('UDPWriter.write() succeeded in writing after series of '
306                                     'failures; resetting warnings.')
307                    self.num_warnings = 0  # we've succeeded
308
309            except (OSError, ConnectionRefusedError) as e:
310                # If we failed, complain, unless we've already complained too much
311                self.good_writes = 0
312                if self.num_warnings < self.warning_limit:
313                    logging.error(f'UDPWriter: send() error: {self.target_str}: {str(e)}')
314                    if 'Message too long' in str(e):
315                        logging.error(f'Message length is {rec_len}')
316                    self.num_warnings += 1
317                    if self.num_warnings == self.warning_limit:
318                        logging.error('UDPWriter.write() - muting errors')
319            num_tries += 1
320
321        logging.debug('UDPWriter.write() wrote %d/%d bytes after %d tries',
322                      bytes_sent, rec_len, num_tries)

Write the record to the network.