445
441
runtime_expansions = ("approval_delay", "approval_duration",
446
"created", "enabled", "expires",
447
"fingerprint", "host", "interval",
448
"last_approval_request", "last_checked_ok",
442
"created", "enabled", "fingerprint",
443
"host", "interval", "last_checked_ok",
449
444
"last_enabled", "name", "timeout")
450
client_defaults = { "timeout": "PT5M",
451
"extended_timeout": "PT15M",
445
client_defaults = { "timeout": "5m",
446
"extended_timeout": "15m",
453
448
"checker": "fping -q -- %%(host)s",
455
"approval_delay": "PT0S",
456
"approval_duration": "PT1S",
450
"approval_delay": "0s",
451
"approval_duration": "1s",
457
452
"approved_by_default": "True",
458
453
"enabled": "True",
577
572
if getattr(self, "enabled", False):
578
573
# Already enabled
575
self.send_changedstate()
580
576
self.expires = datetime.datetime.utcnow() + self.timeout
581
577
self.enabled = True
582
578
self.last_enabled = datetime.datetime.utcnow()
583
579
self.init_checker()
584
self.send_changedstate()
586
581
def disable(self, quiet=True):
587
582
"""Disable this client."""
588
583
if not getattr(self, "enabled", False):
586
self.send_changedstate()
591
588
logger.info("Disabling client %s", self.name)
592
if getattr(self, "disable_initiator_tag", None) is not None:
589
if getattr(self, "disable_initiator_tag", False):
593
590
gobject.source_remove(self.disable_initiator_tag)
594
591
self.disable_initiator_tag = None
595
592
self.expires = None
596
if getattr(self, "checker_initiator_tag", None) is not None:
593
if getattr(self, "checker_initiator_tag", False):
597
594
gobject.source_remove(self.checker_initiator_tag)
598
595
self.checker_initiator_tag = None
599
596
self.stop_checker()
600
597
self.enabled = False
602
self.send_changedstate()
603
598
# Do not run this again if called by a gobject.timeout_add
692
682
self.current_checker_command)
693
683
# Start a new checker if needed
694
684
if self.checker is None:
695
# Escape attributes for the shell
696
escaped_attrs = dict(
697
(attr, re.escape(unicode(getattr(self, attr))))
699
self.runtime_expansions)
701
command = self.checker_command % escaped_attrs
702
except TypeError as error:
703
logger.error('Could not format string "%s"',
704
self.checker_command, exc_info=error)
705
return True # Try again later
686
# In case checker_command has exactly one % operator
687
command = self.checker_command % self.host
689
# Escape attributes for the shell
690
escaped_attrs = dict(
692
re.escape(unicode(str(getattr(self, attr, "")),
696
self.runtime_expansions)
699
command = self.checker_command % escaped_attrs
700
except TypeError as error:
701
logger.error('Could not format string "%s"',
702
self.checker_command, exc_info=error)
703
return True # Try again later
706
704
self.current_checker_command = command
708
706
logger.info("Starting checker %r for %s",
714
712
self.checker = subprocess.Popen(command,
716
714
shell=True, cwd="/")
715
self.checker_callback_tag = (gobject.child_watch_add
717
self.checker_callback,
719
# The checker may have completed before the gobject
720
# watch was added. Check for this.
721
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
723
gobject.source_remove(self.checker_callback_tag)
724
self.checker_callback(pid, status, command)
717
725
except OSError as error:
718
726
logger.error("Failed to start subprocess",
720
self.checker_callback_tag = (gobject.child_watch_add
722
self.checker_callback,
724
# The checker may have completed before the gobject
725
# watch was added. Check for this.
726
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
728
gobject.source_remove(self.checker_callback_tag)
729
self.checker_callback(pid, status, command)
730
728
# Re-run this periodically if run by gobject.timeout_add
1531
1529
def Timeout_dbus_property(self, value=None):
1532
1530
if value is None: # get
1533
1531
return dbus.UInt64(self.timeout_milliseconds())
1534
old_timeout = self.timeout
1535
1532
self.timeout = datetime.timedelta(0, 0, 0, value)
1536
# Reschedule disabling
1533
# Reschedule timeout
1537
1534
if self.enabled:
1538
1535
now = datetime.datetime.utcnow()
1539
self.expires += self.timeout - old_timeout
1540
if self.expires <= now:
1536
time_to_die = timedelta_to_milliseconds(
1537
(self.last_checked_ok + self.timeout) - now)
1538
if time_to_die <= 0:
1541
1539
# The timeout has passed
1542
self.expires = (now +
1543
datetime.timedelta(milliseconds =
1544
1545
if (getattr(self, "disable_initiator_tag", None)
1547
1548
gobject.source_remove(self.disable_initiator_tag)
1548
self.disable_initiator_tag = (
1549
gobject.timeout_add(
1550
timedelta_to_milliseconds(self.expires - now),
1549
self.disable_initiator_tag = (gobject.timeout_add
1553
1553
# ExtendedTimeout - property
1554
1554
@dbus_service_property(_interface, signature="t",
1900
1900
use_ipv6: Boolean; to use IPv6 or not
1902
1902
def __init__(self, server_address, RequestHandlerClass,
1903
interface=None, use_ipv6=True, socketfd=None):
1904
"""If socketfd is set, use that file descriptor instead of
1905
creating a new one with socket.socket().
1903
interface=None, use_ipv6=True):
1907
1904
self.interface = interface
1909
1906
self.address_family = socket.AF_INET6
1910
if socketfd is not None:
1911
# Save the file descriptor
1912
self.socketfd = socketfd
1913
# Save the original socket.socket() function
1914
self.socket_socket = socket.socket
1915
# To implement --socket, we monkey patch socket.socket.
1917
# (When socketserver.TCPServer is a new-style class, we
1918
# could make self.socket into a property instead of monkey
1919
# patching socket.socket.)
1921
# Create a one-time-only replacement for socket.socket()
1922
@functools.wraps(socket.socket)
1923
def socket_wrapper(*args, **kwargs):
1924
# Restore original function so subsequent calls are
1926
socket.socket = self.socket_socket
1927
del self.socket_socket
1928
# This time only, return a new socket object from the
1929
# saved file descriptor.
1930
return socket.fromfd(self.socketfd, *args, **kwargs)
1931
# Replace socket.socket() function with wrapper
1932
socket.socket = socket_wrapper
1933
# The socketserver.TCPServer.__init__ will call
1934
# socket.socket(), which might be our replacement,
1935
# socket_wrapper(), if socketfd was set.
1936
1907
socketserver.TCPServer.__init__(self, server_address,
1937
1908
RequestHandlerClass)
1939
1909
def server_bind(self):
1940
1910
"""This overrides the normal server_bind() function
1941
1911
to bind to an interface if one was specified, and also NOT to
1950
1920
self.socket.setsockopt(socket.SOL_SOCKET,
1951
1921
SO_BINDTODEVICE,
1952
str(self.interface + '\0'))
1953
1924
except socket.error as error:
1954
if error.errno == errno.EPERM:
1955
logger.error("No permission to bind to"
1956
" interface %s", self.interface)
1957
elif error.errno == errno.ENOPROTOOPT:
1925
if error[0] == errno.EPERM:
1926
logger.error("No permission to"
1927
" bind to interface %s",
1929
elif error[0] == errno.ENOPROTOOPT:
1958
1930
logger.error("SO_BINDTODEVICE not available;"
1959
1931
" cannot bind to interface %s",
1960
1932
self.interface)
1961
elif error.errno == errno.ENODEV:
1962
logger.error("Interface %s does not exist,"
1963
" cannot bind", self.interface)
1966
1935
# Only bind(2) the socket if we really need to.
2094
def rfc3339_duration_to_delta(duration):
2095
"""Parse an RFC 3339 "duration" and return a datetime.timedelta
2097
>>> rfc3339_duration_to_delta("P7D")
2098
datetime.timedelta(7)
2099
>>> rfc3339_duration_to_delta("PT60S")
2100
datetime.timedelta(0, 60)
2101
>>> rfc3339_duration_to_delta("PT60M")
2102
datetime.timedelta(0, 3600)
2103
>>> rfc3339_duration_to_delta("PT24H")
2104
datetime.timedelta(1)
2105
>>> rfc3339_duration_to_delta("P1W")
2106
datetime.timedelta(7)
2107
>>> rfc3339_duration_to_delta("PT5M30S")
2108
datetime.timedelta(0, 330)
2109
>>> rfc3339_duration_to_delta("P1DT3M20S")
2110
datetime.timedelta(1, 200)
2113
# Parsing an RFC 3339 duration with regular expressions is not
2114
# possible - there would have to be multiple places for the same
2115
# values, like seconds. The current code, while more esoteric, is
2116
# cleaner without depending on a parsing library. If Python had a
2117
# built-in library for parsing we would use it, but we'd like to
2118
# avoid excessive use of external libraries.
2120
# New type for defining tokens, syntax, and semantics all-in-one
2121
Token = collections.namedtuple("Token",
2122
("regexp", # To match token; if
2123
# "value" is not None,
2124
# must have a "group"
2126
"value", # datetime.timedelta or
2128
"followers")) # Tokens valid after
2130
# RFC 3339 "duration" tokens, syntax, and semantics; taken from
2131
# the "duration" ABNF definition in RFC 3339, Appendix A.
2132
token_end = Token(re.compile(r"$"), None, frozenset())
2133
token_second = Token(re.compile(r"(\d+)S"),
2134
datetime.timedelta(seconds=1),
2135
frozenset((token_end,)))
2136
token_minute = Token(re.compile(r"(\d+)M"),
2137
datetime.timedelta(minutes=1),
2138
frozenset((token_second, token_end)))
2139
token_hour = Token(re.compile(r"(\d+)H"),
2140
datetime.timedelta(hours=1),
2141
frozenset((token_minute, token_end)))
2142
token_time = Token(re.compile(r"T"),
2144
frozenset((token_hour, token_minute,
2146
token_day = Token(re.compile(r"(\d+)D"),
2147
datetime.timedelta(days=1),
2148
frozenset((token_time, token_end)))
2149
token_month = Token(re.compile(r"(\d+)M"),
2150
datetime.timedelta(weeks=4),
2151
frozenset((token_day, token_end)))
2152
token_year = Token(re.compile(r"(\d+)Y"),
2153
datetime.timedelta(weeks=52),
2154
frozenset((token_month, token_end)))
2155
token_week = Token(re.compile(r"(\d+)W"),
2156
datetime.timedelta(weeks=1),
2157
frozenset((token_end,)))
2158
token_duration = Token(re.compile(r"P"), None,
2159
frozenset((token_year, token_month,
2160
token_day, token_time,
2162
# Define starting values
2163
value = datetime.timedelta() # Value so far
2165
followers = frozenset(token_duration,) # Following valid tokens
2166
s = duration # String left to parse
2167
# Loop until end token is found
2168
while found_token is not token_end:
2169
# Search for any currently valid tokens
2170
for token in followers:
2171
match = token.regexp.match(s)
2172
if match is not None:
2174
if token.value is not None:
2175
# Value found, parse digits
2176
factor = int(match.group(1), 10)
2177
# Add to value so far
2178
value += factor * token.value
2179
# Strip token from string
2180
s = token.regexp.sub("", s, 1)
2183
# Set valid next tokens
2184
followers = found_token.followers
2187
# No currently valid tokens were found
2188
raise ValueError("Invalid RFC 3339 duration")
2193
2062
def string_to_delta(interval):
2194
2063
"""Parse a string and return a datetime.timedelta
2339
2195
# Convert the SafeConfigParser object to a dict
2340
2196
server_settings = server_config.defaults()
2341
2197
# Use the appropriate methods on the non-string config options
2342
for option in ("debug", "use_dbus", "use_ipv6", "foreground"):
2198
for option in ("debug", "use_dbus", "use_ipv6"):
2343
2199
server_settings[option] = server_config.getboolean("DEFAULT",
2345
2201
if server_settings["port"]:
2346
2202
server_settings["port"] = server_config.getint("DEFAULT",
2348
if server_settings["socket"]:
2349
server_settings["socket"] = server_config.getint("DEFAULT",
2351
# Later, stdin will, and stdout and stderr might, be dup'ed
2352
# over with an opened os.devnull. But we don't want this to
2353
# happen with a supplied network socket.
2354
if 0 <= server_settings["socket"] <= 2:
2355
server_settings["socket"] = os.dup(server_settings
2357
2204
del server_config
2359
2206
# Override the settings from the config file with command line