=== modified file 'INSTALL' --- INSTALL 2014-07-25 22:17:55 +0000 +++ INSTALL 2014-07-25 22:44:20 +0000 @@ -41,12 +41,11 @@ + GnuTLS 2.4 http://www.gnutls.org/ Note: GnuTLS 3 will only work with Python-GnuTLS 2 + Avahi 0.6.16 http://www.avahi.org/ - + Python 2.6 https://www.python.org/ + + Python 2.7 https://www.python.org/ + Python-GnuTLS 1.1.5 https://pypi.python.org/pypi/python-gnutls/ + dbus-python 0.82.4 http://dbus.freedesktop.org/doc/dbus-python/ + PyGObject 2.14.2 https://developer.gnome.org/pygobject/ + pkg-config http://www.freedesktop.org/wiki/Software/pkg-config/ - + Python-argparse https://pypi.python.org/pypi/argparse + Urwid 1.0.1 http://urwid.org/ (Only needed by the "mandos-monitor" tool.) === modified file 'debian/control' --- debian/control 2014-07-12 12:54:13 +0000 +++ debian/control 2014-07-25 22:44:20 +0000 @@ -7,9 +7,8 @@ Build-Depends: debhelper (>= 9), docbook-xml, docbook-xsl, libavahi-core-dev, libgpgme11-dev, libgnutls-dev, xsltproc, pkg-config -Build-Depends-Indep: systemd, python (>=2.6), python-gnutls, - python-dbus, python-avahi, python-gobject, - python (>=2.7) | python-argparse +Build-Depends-Indep: systemd, python2.7, python2.7-gnutls, + python2.7-dbus, python2.7-avahi, python2.7-gobject Standards-Version: 3.9.5 Vcs-Bzr: http://ftp.recompile.se/pub/mandos/trunk Vcs-Browser: http://bzr.recompile.se/loggerhead/mandos/trunk/files @@ -17,10 +16,11 @@ Package: mandos Architecture: all -Depends: ${misc:Depends}, python (>=2.6), python-gnutls, python-dbus, - python-avahi, python-gobject, avahi-daemon, adduser, - python-urwid, python (>=2.7) | python-argparse, gnupg (<< 2), - initscripts (>= 2.88dsf-13.3) +Depends: ${misc:Depends}, python (<= 2.7), python2.7, python-gnutls, + python2.7-gnutls, python-dbus, python2.7-dbus, python-avahi, + python2.7-avahi, python-gobject, python2.7-gobject, + avahi-daemon, adduser, python-urwid, python2.7-urwid, + gnupg (<< 2), initscripts (>= 2.88dsf-13.3) Recommends: ssh-client | fping Description: server giving encrypted passwords to Mandos clients This is the server part of the Mandos system, which allows === modified file 'mandos' --- mandos 2014-07-24 01:35:55 +0000 +++ mandos 2014-07-25 23:39:49 +0000 @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2.7 # -*- mode: python; coding: utf-8 -*- # # Mandos server - give out binary blobs to connecting clients. @@ -338,7 +338,7 @@ elif state == avahi.ENTRY_GROUP_FAILURE: logger.critical("Avahi: Error in group state changed %s", unicode(error)) - raise AvahiGroupError("State changed: {0!s}" + raise AvahiGroupError("State changed: {!s}" .format(error)) def cleanup(self): @@ -395,19 +395,12 @@ """Add the new name to the syslog messages""" ret = AvahiService.rename(self) syslogger.setFormatter(logging.Formatter - ('Mandos ({0}) [%(process)d]:' + ('Mandos ({}) [%(process)d]:' ' %(levelname)s: %(message)s' .format(self.name))) return ret -def timedelta_to_milliseconds(td): - "Convert a datetime.timedelta() to milliseconds" - return ((td.days * 24 * 60 * 60 * 1000) - + (td.seconds * 1000) - + (td.microseconds // 1000)) - - class Client(object): """A representation of a client host served by this server. @@ -468,21 +461,6 @@ "enabled": "True", } - def timeout_milliseconds(self): - "Return the 'timeout' attribute in milliseconds" - return timedelta_to_milliseconds(self.timeout) - - def extended_timeout_milliseconds(self): - "Return the 'extended_timeout' attribute in milliseconds" - return timedelta_to_milliseconds(self.extended_timeout) - - def interval_milliseconds(self): - "Return the 'interval' attribute in milliseconds" - return timedelta_to_milliseconds(self.interval) - - def approval_delay_milliseconds(self): - return timedelta_to_milliseconds(self.approval_delay) - @staticmethod def config_parser(config): """Construct a new dict of client settings of this form: @@ -513,7 +491,7 @@ "rb") as secfile: client["secret"] = secfile.read() else: - raise TypeError("No secret or secfile for section {0}" + raise TypeError("No secret or secfile for section {}" .format(section)) client["timeout"] = string_to_delta(section["timeout"]) client["extended_timeout"] = string_to_delta( @@ -536,7 +514,7 @@ server_settings = {} self.server_settings = server_settings # adding all client settings - for setting, value in settings.iteritems(): + for setting, value in settings.items(): setattr(self, setting, value) if self.enabled: @@ -625,14 +603,16 @@ if self.checker_initiator_tag is not None: gobject.source_remove(self.checker_initiator_tag) self.checker_initiator_tag = (gobject.timeout_add - (self.interval_milliseconds(), + (int(self.interval + .total_seconds() * 1000), self.start_checker)) # Schedule a disable() when 'timeout' has passed if self.disable_initiator_tag is not None: gobject.source_remove(self.disable_initiator_tag) self.disable_initiator_tag = (gobject.timeout_add - (self.timeout_milliseconds(), - self.disable)) + (int(self.timeout + .total_seconds() * 1000), + self.disable)) # Also start a new checker *right now*. self.start_checker() @@ -669,8 +649,8 @@ self.disable_initiator_tag = None if getattr(self, "enabled", False): self.disable_initiator_tag = (gobject.timeout_add - (timedelta_to_milliseconds - (timeout), self.disable)) + (int(timeout.total_seconds() + * 1000), self.disable)) self.expires = datetime.datetime.utcnow() + timeout def need_approval(self): @@ -707,10 +687,10 @@ # Start a new checker if needed if self.checker is None: # Escape attributes for the shell - escaped_attrs = dict( - (attr, re.escape(unicode(getattr(self, attr)))) - for attr in - self.runtime_expansions) + escaped_attrs = { attr: + re.escape(unicode(getattr(self, + attr))) + for attr in self.runtime_expansions } try: command = self.checker_command % escaped_attrs except TypeError as error: @@ -797,7 +777,7 @@ # "Set" method, so we fail early here: if byte_arrays and signature != "ay": raise ValueError("Byte arrays not supported for non-'ay'" - " signature {0!r}".format(signature)) + " signature {!r}".format(signature)) def decorator(func): func._dbus_is_property = True func._dbus_interface = dbus_interface @@ -882,7 +862,7 @@ If called like _is_dbus_thing("method") it returns a function suitable for use as predicate to inspect.getmembers(). """ - return lambda obj: getattr(obj, "_dbus_is_{0}".format(thing), + return lambda obj: getattr(obj, "_dbus_is_{}".format(thing), False) def _get_all_dbus_things(self, thing): @@ -938,7 +918,7 @@ # signatures other than "ay". if prop._dbus_signature != "ay": raise ValueError("Byte arrays not supported for non-" - "'ay' signature {0!r}" + "'ay' signature {!r}" .format(prop._dbus_signature)) value = dbus.ByteArray(b''.join(chr(byte) for byte in value)) @@ -1009,7 +989,7 @@ (prop, "_dbus_annotations", {})) - for name, value in annots.iteritems(): + for name, value in annots.items(): ann_tag = document.createElement( "annotation") ann_tag.setAttribute("name", name) @@ -1018,11 +998,11 @@ # Add interface annotation tags for annotation, value in dict( itertools.chain.from_iterable( - annotations().iteritems() + annotations().items() for name, annotations in self._get_all_dbus_things("interface") if name == if_tag.getAttribute("name") - )).iteritems(): + )).items(): ann_tag = document.createElement("annotation") ann_tag.setAttribute("name", annotation) ann_tag.setAttribute("value", value) @@ -1084,7 +1064,7 @@ """ def wrapper(cls): for orig_interface_name, alt_interface_name in ( - alt_interface_names.iteritems()): + alt_interface_names.items()): attr = {} interface_names = set() # Go though all attributes of the class @@ -1207,7 +1187,7 @@ attribute.func_closure))) if deprecate: # Deprecate all alternate interfaces - iname="_AlternateDBusNames_interface_annotation{0}" + iname="_AlternateDBusNames_interface_annotation{}" for interface_name in interface_names: @dbus_interface_annotations(interface_name) def func(self): @@ -1222,7 +1202,7 @@ if interface_names: # Replace the class with a new subclass of it with # methods, signals, etc. as created above. - cls = type(b"{0}Alternate".format(cls.__name__), + cls = type(b"{}Alternate".format(cls.__name__), (cls,), attr) return cls return wrapper @@ -1269,7 +1249,7 @@ to the D-Bus. Default: no transform variant_level: D-Bus variant level. Default: 1 """ - attrname = "_{0}".format(dbus_name) + attrname = "_{}".format(dbus_name) def setter(self, value): if hasattr(self, "dbus_object_path"): if (not hasattr(self, attrname) or @@ -1305,21 +1285,23 @@ approval_delay = notifychangeproperty(dbus.UInt64, "ApprovalDelay", type_func = - timedelta_to_milliseconds) + lambda td: td.total_seconds() + * 1000) approval_duration = notifychangeproperty( dbus.UInt64, "ApprovalDuration", - type_func = timedelta_to_milliseconds) + type_func = lambda td: td.total_seconds() * 1000) host = notifychangeproperty(dbus.String, "Host") timeout = notifychangeproperty(dbus.UInt64, "Timeout", - type_func = - timedelta_to_milliseconds) + type_func = lambda td: + td.total_seconds() * 1000) extended_timeout = notifychangeproperty( dbus.UInt64, "ExtendedTimeout", - type_func = timedelta_to_milliseconds) + type_func = lambda td: td.total_seconds() * 1000) interval = notifychangeproperty(dbus.UInt64, "Interval", type_func = - timedelta_to_milliseconds) + lambda td: td.total_seconds() + * 1000) checker_command = notifychangeproperty(dbus.String, "Checker") del notifychangeproperty @@ -1368,9 +1350,8 @@ def approve(self, value=True): self.approved = value - gobject.timeout_add(timedelta_to_milliseconds - (self.approval_duration), - self._reset_approved) + gobject.timeout_add(int(self.approval_duration.total_seconds() + * 1000), self._reset_approved) self.send_changedstate() ## D-Bus methods, signals & properties @@ -1479,7 +1460,8 @@ access="readwrite") def ApprovalDelay_dbus_property(self, value=None): if value is None: # get - return dbus.UInt64(self.approval_delay_milliseconds()) + return dbus.UInt64(self.approval_delay.total_seconds() + * 1000) self.approval_delay = datetime.timedelta(0, 0, 0, value) # ApprovalDuration - property @@ -1487,8 +1469,8 @@ access="readwrite") def ApprovalDuration_dbus_property(self, value=None): if value is None: # get - return dbus.UInt64(timedelta_to_milliseconds( - self.approval_duration)) + return dbus.UInt64(self.approval_duration.total_seconds() + * 1000) self.approval_duration = datetime.timedelta(0, 0, 0, value) # Name - property @@ -1560,7 +1542,7 @@ access="readwrite") def Timeout_dbus_property(self, value=None): if value is None: # get - return dbus.UInt64(self.timeout_milliseconds()) + return dbus.UInt64(self.timeout.total_seconds() * 1000) old_timeout = self.timeout self.timeout = datetime.timedelta(0, 0, 0, value) # Reschedule disabling @@ -1577,15 +1559,16 @@ gobject.source_remove(self.disable_initiator_tag) self.disable_initiator_tag = ( gobject.timeout_add( - timedelta_to_milliseconds(self.expires - now), - self.disable)) + int((self.expires - now).total_seconds() + * 1000), self.disable)) # ExtendedTimeout - property @dbus_service_property(_interface, signature="t", access="readwrite") def ExtendedTimeout_dbus_property(self, value=None): if value is None: # get - return dbus.UInt64(self.extended_timeout_milliseconds()) + return dbus.UInt64(self.extended_timeout.total_seconds() + * 1000) self.extended_timeout = datetime.timedelta(0, 0, 0, value) # Interval - property @@ -1593,7 +1576,7 @@ access="readwrite") def Interval_dbus_property(self, value=None): if value is None: # get - return dbus.UInt64(self.interval_milliseconds()) + return dbus.UInt64(self.interval.total_seconds() * 1000) self.interval = datetime.timedelta(0, 0, 0, value) if getattr(self, "checker_initiator_tag", None) is None: return @@ -1759,8 +1742,8 @@ if self.server.use_dbus: # Emit D-Bus signal client.NeedApproval( - client.approval_delay_milliseconds(), - client.approved_by_default) + client.approval_delay.total_seconds() + * 1000, client.approved_by_default) else: logger.warning("Client %s was not approved", client.name) @@ -1772,9 +1755,7 @@ #wait until timeout or approved time = datetime.datetime.now() client.changedstate.acquire() - client.changedstate.wait( - float(timedelta_to_milliseconds(delay) - / 1000)) + client.changedstate.wait(delay.total_seconds()) client.changedstate.release() time2 = datetime.datetime.now() if (time2 - time) >= delay: @@ -2258,7 +2239,7 @@ elif suffix == "w": delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value) else: - raise ValueError("Unknown suffix {0!r}" + raise ValueError("Unknown suffix {!r}" .format(suffix)) except IndexError as e: raise ValueError(*(e.args)) @@ -2281,8 +2262,7 @@ # Close all standard open file descriptors null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR) if not stat.S_ISCHR(os.fstat(null).st_mode): - raise OSError(errno.ENODEV, - "{0} not a character device" + raise OSError(errno.ENODEV, "{} not a character device" .format(os.devnull)) os.dup2(null, sys.stdin.fileno()) os.dup2(null, sys.stdout.fileno()) @@ -2298,7 +2278,7 @@ parser = argparse.ArgumentParser() parser.add_argument("-v", "--version", action="version", - version = "%(prog)s {0}".format(version), + version = "%(prog)s {}".format(version), help="show version number and exit") parser.add_argument("-i", "--interface", metavar="IF", help="Bind to interface IF") @@ -2443,7 +2423,7 @@ if server_settings["servicename"] != "Mandos": syslogger.setFormatter(logging.Formatter - ('Mandos ({0}) [%(process)d]:' + ('Mandos ({}) [%(process)d]:' ' %(levelname)s: %(message)s' .format(server_settings ["servicename"]))) @@ -2583,7 +2563,7 @@ os.remove(stored_state_path) except IOError as e: if e.errno == errno.ENOENT: - logger.warning("Could not load persistent state: {0}" + logger.warning("Could not load persistent state: {}" .format(os.strerror(e.errno))) else: logger.critical("Could not load persistent state:", @@ -2594,7 +2574,7 @@ "EOFError:", exc_info=e) with PGPEngine() as pgp: - for client_name, client in clients_data.iteritems(): + for client_name, client in clients_data.items(): # Skip removed clients if client_name not in client_settings: continue @@ -2625,14 +2605,14 @@ if datetime.datetime.utcnow() >= client["expires"]: if not client["last_checked_ok"]: logger.warning( - "disabling client {0} - Client never " + "disabling client {} - Client never " "performed a successful checker" .format(client_name)) client["enabled"] = False elif client["last_checker_status"] != 0: logger.warning( - "disabling client {0} - Client " - "last checker failed with error code {1}" + "disabling client {} - Client last" + " checker failed with error code {}" .format(client_name, client["last_checker_status"])) client["enabled"] = False @@ -2641,7 +2621,7 @@ .utcnow() + client["timeout"]) logger.debug("Last checker succeeded," - " keeping {0} enabled" + " keeping {} enabled" .format(client_name)) try: client["secret"] = ( @@ -2650,7 +2630,7 @@ ["secret"])) except PGPError: # If decryption fails, we use secret from new settings - logger.debug("Failed to decrypt {0} old secret" + logger.debug("Failed to decrypt {} old secret" .format(client_name)) client["secret"] = ( client_settings[client_name]["secret"]) @@ -2664,7 +2644,7 @@ clients_data[client_name] = client_settings[client_name] # Create all client objects - for client_name, client in clients_data.iteritems(): + for client_name, client in clients_data.items(): tcp_server.clients[client_name] = client_class( name = client_name, settings = client, server_settings = server_settings) @@ -2774,8 +2754,8 @@ # A list of attributes that can not be pickled # + secret. - exclude = set(("bus", "changedstate", "secret", - "checker", "server_settings")) + exclude = { "bus", "changedstate", "secret", + "checker", "server_settings" } for name, typ in (inspect.getmembers (dbus.service.Object)): exclude.add(name) @@ -2804,7 +2784,7 @@ except NameError: pass if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST): - logger.warning("Could not save persistent state: {0}" + logger.warning("Could not save persistent state: {}" .format(os.strerror(e.errno))) else: logger.warning("Could not save persistent state:", === modified file 'mandos-ctl' --- mandos-ctl 2014-07-24 01:35:55 +0000 +++ mandos-ctl 2014-07-25 23:42:53 +0000 @@ -42,7 +42,7 @@ import dbus -if sys.version_info[0] == 2: +if sys.version_info.major == 2: str = unicode locale.setlocale(locale.LC_ALL, "") @@ -74,16 +74,10 @@ client_interface = domain + ".Mandos.Client" version = "1.6.7" -def timedelta_to_milliseconds(td): - """Convert a datetime.timedelta object to milliseconds""" - return ((td.days * 24 * 60 * 60 * 1000) - + (td.seconds * 1000) - + (td.microseconds // 1000)) - def milliseconds_to_string(ms): td = datetime.timedelta(0, 0, 0, ms) return ("{days}{hours:02}:{minutes:02}:{seconds:02}" - .format(days = "{0}T".format(td.days) if td.days else "", + .format(days = "{}T".format(td.days) if td.days else "", hours = td.seconds // 3600, minutes = (td.seconds % 3600) // 60, seconds = td.seconds % 60, @@ -249,10 +243,10 @@ # Print header line print(format_string.format(**tablewords)) for client in clients: - print(format_string.format(**dict((key, + print(format_string.format(**{ key: valuetostring(client[key], - key)) - for key in keywords))) + key) + for key in keywords })) def has_actions(options): return any((options.enable, @@ -277,7 +271,7 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument("--version", action="version", - version = "%(prog)s {0}".format(version), + version = "%(prog)s {}".format(version), help="show version number and exit") parser.add_argument("-a", "--all", action="store_true", help="Select all clients") @@ -372,18 +366,17 @@ clients={} if options.all or not options.client: - clients = dict((bus.get_object(busname, path), properties) - for path, properties in - mandos_clients.items()) + clients = { bus.get_object(busname, path): properties + for path, properties in mandos_clients.items() } else: for name in options.client: - for path, client in mandos_clients.iteritems(): + for path, client in mandos_clients.items(): if client["Name"] == name: client_objc = bus.get_object(busname, path) clients[client_objc] = client break else: - print("Client not found on server: {0!r}" + print("Client not found on server: {!r}" .format(name), file=sys.stderr) sys.exit(1) @@ -412,8 +405,8 @@ """Set a Client D-Bus property, converted from a string to milliseconds.""" set_client_prop(prop, - timedelta_to_milliseconds - (string_to_delta(value))) + string_to_delta(value).total_seconds() + * 1000) if options.remove: mandos_serv.RemoveClient(client.__dbus_object_path__) if options.enable: === modified file 'mandos-monitor' --- mandos-monitor 2014-07-24 02:58:45 +0000 +++ mandos-monitor 2014-07-25 23:42:53 +0000 @@ -48,7 +48,7 @@ import locale -if sys.version_info[0] == 2: +if sys.version_info.major == 2: str = unicode locale.setlocale(locale.LC_ALL, '') @@ -161,7 +161,7 @@ self.rejected, client_interface, byte_arrays=True)) - self.logger('Created client {0}' + self.logger('Created client {}' .format(self.properties["Name"]), level=0) def using_timer(self, flag): @@ -179,52 +179,52 @@ def checker_completed(self, exitstatus, condition, command): if exitstatus == 0: - self.logger('Checker for client {0} (command "{1}")' + self.logger('Checker for client {} (command "{}")' ' succeeded'.format(self.properties["Name"], command), level=0) self.update() return # Checker failed if os.WIFEXITED(condition): - self.logger('Checker for client {0} (command "{1}")' - ' failed with exit code {2}' + self.logger('Checker for client {} (command "{}") failed' + ' with exit code {}' .format(self.properties["Name"], command, os.WEXITSTATUS(condition))) elif os.WIFSIGNALED(condition): - self.logger('Checker for client {0} (command "{1}") was' - ' killed by signal {2}' + self.logger('Checker for client {} (command "{}") was' + ' killed by signal {}' .format(self.properties["Name"], command, os.WTERMSIG(condition))) elif os.WCOREDUMP(condition): - self.logger('Checker for client {0} (command "{1}")' - ' dumped core' - .format(self.properties["Name"], command)) + self.logger('Checker for client {} (command "{}") dumped' + ' core'.format(self.properties["Name"], + command)) else: - self.logger('Checker for client {0} completed' + self.logger('Checker for client {} completed' ' mysteriously' .format(self.properties["Name"])) self.update() def checker_started(self, command): """Server signals that a checker started.""" - self.logger('Client {0} started checker "{1}"' + self.logger('Client {} started checker "{}"' .format(self.properties["Name"], command), level=0) def got_secret(self): - self.logger('Client {0} received its secret' + self.logger('Client {} received its secret' .format(self.properties["Name"])) def need_approval(self, timeout, default): if not default: - message = 'Client {0} needs approval within {1} seconds' + message = 'Client {} needs approval within {} seconds' else: - message = 'Client {0} will get its secret in {1} seconds' + message = 'Client {} will get its secret in {} seconds' self.logger(message.format(self.properties["Name"], timeout/1000)) def rejected(self, reason): - self.logger('Client {0} was rejected; reason: {1}' + self.logger('Client {} was rejected; reason: {}' .format(self.properties["Name"], reason)) def selectable(self): @@ -274,9 +274,9 @@ else: timer = datetime.timedelta() if self.properties["ApprovedByDefault"]: - message = "Approval in {0}. (d)eny?" + message = "Approval in {}. (d)eny?" else: - message = "Denial in {0}. (a)pprove?" + message = "Denial in {}. (a)pprove?" message = message.format(str(timer).rsplit(".", 1)[0]) self.using_timer(True) elif self.properties["LastCheckerStatus"] != 0: @@ -290,13 +290,13 @@ timer = max(expires - datetime.datetime.utcnow(), datetime.timedelta()) message = ('A checker has failed! Time until client' - ' gets disabled: {0}' + ' gets disabled: {}' .format(str(timer).rsplit(".", 1)[0])) self.using_timer(True) else: message = "enabled" self.using_timer(False) - self._text = "{0}{1}".format(base, message) + self._text = "{}{}".format(base, message) if not urwid.supports_unicode(): self._text = self._text.encode("ascii", "replace") @@ -469,8 +469,8 @@ self.main_loop = gobject.MainLoop() def client_not_found(self, fingerprint, address): - self.log_message("Client with address {0} and fingerprint" - " {1} could not be found" + self.log_message("Client with address {} and fingerprint {}" + " could not be found" .format(address, fingerprint)) def rebuild(self): @@ -494,7 +494,7 @@ if level < self.log_level: return timestamp = datetime.datetime.now().isoformat() - self.log_message_raw("{0}: {1}".format(timestamp, message), + self.log_message_raw("{}: {}".format(timestamp, message), level=level) def log_message_raw(self, markup, level=1): @@ -513,7 +513,7 @@ """Toggle visibility of the log buffer.""" self.log_visible = not self.log_visible self.rebuild() - self.log_message("Log visibility changed to: {0}" + self.log_message("Log visibility changed to: {}" .format(self.log_visible), level=0) def change_log_display(self): @@ -525,7 +525,7 @@ self.log_wrap = "clip" for textwidget in self.log: textwidget.set_wrap_mode(self.log_wrap) - self.log_message("Wrap mode: {0}".format(self.log_wrap), + self.log_message("Wrap mode: {}".format(self.log_wrap), level=0) def find_and_remove_client(self, path, name): @@ -537,7 +537,7 @@ client = self.clients_dict[path] except KeyError: # not found? - self.log_message("Unknown client {0!r} ({1!r}) removed" + self.log_message("Unknown client {!r} ({!r}) removed" .format(name, path)) return client.delete()