Ejemplo n.º 1
0
 def __init__(self):
     self.env = Environment.getInstance()
     self.log = logging.getLogger(__name__)
     self.subtopic = "%s/proxy" % self.env.domain
     self.mqtt = MQTTHandler(host=self.env.config.get("mqtt.host"),
                             port=self.env.config.getint("mqtt.port",
                                                         default=1883))
Ejemplo n.º 2
0
class MqttEventConsumer(BaseEventConsumer):
    """
    Subscribes to the external MQTT event topic and forwards incoming events to the given callback
    """
    def __init__(self, callback=None, event_type=None):
        super(MqttEventConsumer, self).__init__(callback, event_type)
        self.mqtt = MQTTHandler()
        self.mqtt.get_client().add_subscription('%s/events' % Environment.getInstance().domain)
        self.mqtt.set_subscription_callback(self.__process_event)

    def __process_event(self, topic, message):
        super(MqttEventConsumer, self)._process_event(etree.fromstring(message, PluginRegistry.getEventParser()))
Ejemplo n.º 3
0
    def serve(self):
        self.mqtt = MQTTHandler(host=self.env.config.get("mqtt.host"),
                                port=self.env.config.getint("mqtt.port", default=1883),
                                client_id_prefix="MQTTRPCService")

        self.__command_registry = PluginRegistry.getInstance('CommandRegistry')
        self.log.info("MQTT RPC service started, listening on subtopic '%s/#'" % self.subtopic)
        self.mqtt.get_client().add_subscription('%s/#' % self.subtopic, qos=2, callback=self.handle_request)
Ejemplo n.º 4
0
    def serve(self):
        self.backend_mqtt = MQTTHandler(
            host=self.env.config.get("backend.mqtt-host"),
            port=self.env.config.getint("backend.mqtt-port", default=1883))

        # subscribe to all client relevant topics
        self.backend_mqtt.get_client().add_subscription("%s/client/#" % self.env.domain, qos=1)
        # subscribe to proxy topic
        self.backend_mqtt.get_client().add_subscription("%s/proxy" % self.env.domain, qos=1)
        self.backend_mqtt.set_subscription_callback(self._handle_backend_message)

        # set our last will and testament
        e = EventMaker()
        goodbye = e.Event(e.ClientLeave(e.Id(self.env.core_uuid)))
        self.backend_mqtt.will_set("%s/proxy" % self.env.domain, goodbye, qos=1)

        # connect to the proxy MQTT broker (where the clients are listening)
        self.proxy_mqtt = MQTTHandler(
            host=self.env.config.get("mqtt.host"),
            port=self.env.config.getint("mqtt.port", default=1883))
        self.proxy_mqtt.get_client().add_subscription("%s/client/#" % self.env.domain, qos=1)
        self.proxy_mqtt.set_subscription_callback(self._handle_proxy_message)
Ejemplo n.º 5
0
    def serve(self):
        """
        Start serving the command registry to the outside world. Send
        hello and register event callbacks.
        """

        for clazz in PluginRegistry.modules.values():
            for mname, method in getmembers(clazz):
                if ismethod(method) and hasattr(method, "isCommand"):
                    func = mname

                    # Adjust documentation
                    if not method.__help__:
                        raise CommandInvalid(
                            C.make_error("COMMAND_WITHOUT_DOCS", method=func))

                    doc = re.sub("(\s|\n)+", " ", method.__help__).strip()

                    self.log.debug("registering %s" % func)
                    info = {
                        'name':
                        func,
                        'path':
                        "%s.%s" % (clazz.__class__.__name__, mname),
                        'sig': [] if not getargspec(method).args else
                        getargspec(method).args,
                        'target':
                        clazz.get_target(),
                        'type':
                        getattr(method, "type", NORMAL),
                        'doc':
                        doc,
                    }

                    if 'self' in info['sig']:
                        info['sig'].remove('self')

                    self.commands[func] = info

        if self.env.mode == "proxy":
            # initializing MQTTServiceProxy to GOsa backend
            self.mqtt = MQTTHandler(
                host=self.env.config.get("backend.mqtt-host"),
                port=self.env.config.getint("backend.mqtt-port", default=1883))
            queue = '%s/proxy/%s' % (self.env.domain, self.env.uuid)
            self.backend_proxy = MQTTServiceProxy(mqttHandler=self.mqtt,
                                                  serviceAddress=queue,
                                                  methods=False)
Ejemplo n.º 6
0
    def __init__(self,
                 mqttHandler=None,
                 serviceAddress=None,
                 serviceName=None,
                 methods=None):
        self.__handler = mqttHandler if mqttHandler is not None else MQTTHandler(
        )
        self.__serviceName = serviceName
        self.__serviceAddress = serviceAddress
        self.__methods = methods

        # Retrieve methods
        if not self.__methods:
            self.__serviceName = "getMethods"
            self.__methods = self.__call__()
            self.__serviceName = None
Ejemplo n.º 7
0
    def __init__(self,
                 mqttHandler=None,
                 serviceAddress=None,
                 serviceName=None,
                 methods=None):
        self.__handler = mqttHandler if mqttHandler is not None else MQTTHandler(
        )
        self.__serviceName = serviceName
        self.__serviceAddress = serviceAddress
        self.__methods = methods
        self.env = Environment.getInstance()

        # Retrieve methods
        if self.__methods is None:
            self.__serviceName = "getMethods"
            self.__methods = self.__call__()
            self.__serviceName = None
Ejemplo n.º 8
0
 def __init__(self, callback=None, event_type=None):
     super(MqttEventConsumer, self).__init__(callback, event_type)
     self.mqtt = MQTTHandler()
     self.mqtt.get_client().add_subscription('%s/events' % Environment.getInstance().domain)
     self.mqtt.set_subscription_callback(self.__process_event)
Ejemplo n.º 9
0
class ClientService(Plugin):
    """
    Plugin to register clients and expose their functionality
    to the users.

    Keys for configuration section **goto**

    +------------------+------------+-------------------------------------------------------------+
    + Key              | Format     +  Description                                                |
    +==================+============+=============================================================+
    + machine-rdn      | String     + RDN to initially place new machines in.                     |
    +------------------+------------+-------------------------------------------------------------+
    + timeout          | Integer    + Client ping interval.                                       |
    +------------------+------------+-------------------------------------------------------------+

    """
    
    _priority_ = 90
    _target_ = 'goto'
    __client = {}
    __proxy = {}
    __user_session = {}
    __listeners = {}
    entry_attributes = ['cn', 'description', 'gosaApplicationPriority', 'gosaApplicationIcon', 'gosaApplicationName', 'gotoLogonScript', 'gosaApplicationFlags', 'gosaApplicationExecute']
    entry_map = {"gosaApplicationPriority": "prio", "description": "description"}
    printer_attributes = ["gotoPrinterPPD", "labeledURI", "cn", "l", "description"]
    __client_call_queue = {}
    ppd_proxy = None
    __current_backend_rpc = None
    # changes that need to be verified by the proxy to know if its in sync
    __acl_change_checks = []

    def __init__(self):
        """
        Construct a new ClientService instance based on the configuration
        stored in the environment.
        """
        env = Environment.getInstance()
        self.log = logging.getLogger(__name__)
        self.log.info("initializing client service")
        self.env = env
        self.__cr = None
        self.mqtt = None

    def __get_handler(self):
        if self.mqtt is None:
            self.mqtt = MQTTHandler(client_id_prefix="ClientService")
        return self.mqtt

    def serve(self):
        # Add event processor
        mqtt = self.__get_handler()
        # listen to client topics
        mqtt.get_client().add_subscription('%s/client/+' % self.env.domain, qos=1)
        self.log.debug("subscribing to %s event queue on %s" % ('%s/client/+' % self.env.domain, mqtt.host))
        mqtt.set_subscription_callback(self.__eventProcessor)

        # Get registry - we need it later on
        self.__cr = PluginRegistry.getInstance("CommandRegistry")

        # Start maintenance when index scan is finished
        zope.event.subscribers.append(self.__handle_events)

        # Register scheduler task to remove outdated clients
        sched = PluginRegistry.getInstance('SchedulerService').getScheduler()
        sched.add_interval_job(self.__gc, minutes=1, tag='_internal', jobstore="ram")

        # self.register_listener("configureHostPrinters", self._on_client_caps)
        self.ppd_proxy = PluginRegistry.getInstance("PPDProxy")

    def __handle_events(self, event):
        """
        React on object modifications to keep active ACLs up to date.
        """
        if event.__class__.__name__ == "IndexSyncFinished":
            self.__refresh()

        elif event.__class__.__name__ == "ACLChanged":
            if self.env.mode != "proxy":
                e = EventMaker()
                checks = self.__acl_change_checks
                self.__acl_change_checks = []
                trigger = e.Event(
                    e.Trigger(
                        e.Type(event.__class__.__name__),
                        e.Check(json.dumps(checks))
                    )
                )
                self.mqtt.send_event(trigger, "%s/proxy" % self.env.domain)

    def __refresh(self):
        # Initially check if we need to ask for client caps
        if not self.__client:
            e = EventMaker()
            self.mqtt.send_event(e.Event(e.ClientPoll()), "%s/client/broadcast" % self.env.domain)

    def stop(self):  # pragma: nocover
        pass

    def get_client_uuid(self, name_or_uuid):
        if is_uuid(name_or_uuid):
            return name_or_uuid
        else:
            # hostname used
            for uuid in self.__client:
                if self.__client[uuid]["name"] == name_or_uuid:
                    return uuid
        return name_or_uuid

    @Command(__help__=N_("List available clients."), type="READONLY")
    def getClients(self):
        """
        List available domain clients.

        ``Return:`` dict with name and timestamp information, indexed by UUID
        """
        res = {}
        for uuid, info in self.__client.items():
            if info['online']:
                res[uuid] = {'name': info['name'], 'last-seen': info['last-seen']}
        return res

    @gen.coroutine
    @Command(__help__=N_("Call method exposed by client."))
    def clientDispatch(self, client, method, *arg, **larg):
        """
        Dispatch a method on the client.

        ========= ================================
        Parameter Description
        ========= ================================
        client    Device UUID of the client
        method    Method name to call
        *         Method arguments
        ========= ================================

        ``Return:`` varies
        """
        client = self.get_client_uuid(client)

        # Bail out if the client is not available
        if not client in self.__client:
            raise JSONRPCException("client '%s' not available" % client)
        if not self.__client[client]['online']:
            raise JSONRPCException("client '%s' is offline" % client)
        if not method in self.__client[client]['caps']:
            raise JSONRPCException("client '%s' has no method '%s' exported" % (client, method))

        # Generate tag queue name
        queue = '%s/client/%s' % (self.env.domain, client)
        self.log.debug("got client dispatch: '%s(%s)', sending to %s" % (method, arg, queue))

        # client queue -> mqtt rpc proxy
        if not client in self.__proxy:
            self.__proxy[client] = MQTTServiceProxy(mqttHandler=self.mqtt, serviceAddress=queue)

        # Call her to the moon...
        methodCall = getattr(self.__proxy[client], method)

        # Do the call
        res = yield methodCall(*arg, **larg)
        raise gen.Return(res)

    @Command(__help__=N_("Check if the client supports a method call"), type="READONLY")
    def hasCapability(self, client_id, method):
        return client_id in self.__client and method in self.__client[client_id]

    def queuedClientDispatch(self, client, method, *arg, **larg):
        client = self.get_client_uuid(client)

        # Bail out if the client is not available
        if client not in self.__client:
            raise JSONRPCException("client '%s' not available" % client)
        if not self.__client[client]['online']:
            raise JSONRPCException("client '%s' is offline" % client)
        if method not in self.__client[client]['caps']:
            # wait til method gets available
            if client not in self.__client_call_queue:
                self.__client_call_queue[client] = {}
            if method not in self.__client_call_queue[client]:
                self.__client_call_queue[client][method] = []
            if method not in self.__listeners:
                self.register_listener(method, self._on_client_caps)
            self.__client_call_queue[client][method].append((arg, larg))
        else:
            self.clientDispatch(client, method, *arg, **larg)

    @Command(__help__=N_("Get the client Interface/IP/Netmask/Broadcast/MAC list."), type="READONLY")
    def getClientNetInfo(self, client):
        """
        Get brief information about the client network setup.

        Example:

        .. doctest::

            >>> getClientNetInfo("eb5e72d4-c53f-4612-81a3-602b14a8da69")
            {'eth0': {
                'Broadcast': '10.89.1.255',
                'MAC': '00:01:6c:9d:b9:fa',
                'IPAddress': '10.89.1.31',
                'Netmask': '255.255.255.0',
                'IPv6Address': 'fe80::201:6cff:fe9d:b9fa/64'}}

        ``Return:`` dict with network information
        """
        client = self.get_client_uuid(client)
        if not client in self.__client:
            return []

        res = self.__client[client]['network']
        return res

    @Command(__help__=N_("List available client methods for specified client."), type="READONLY")
    def getClientMethods(self, client):
        """
        Get list of available client methods and their signature.

        ``Return:`` dict of client methods
        """
        client = self.get_client_uuid(client)

        if not client in self.__client:
            return []
        if not self.__client[client]['online']:
            return []

        return self.__client[client]['caps']

    @Command(__help__=N_("List user sessions per client"), type="READONLY")
    def getUserSessions(self, client=None):
        """
        TODO
        """
        if client:
            client = self.get_client_uuid(client)
            return list(self.__user_session[client]) if client in self.__user_session else []

        return list(self.__user_session)

    @Command(__help__=N_("List clients a user is logged in"), type="READONLY")
    def getUserClients(self, user):
        """
        TODO
        """
        return [client for client, users in self.__user_session.items() if user in users]

    @Command(__help__=N_("Get the destinationIndicator for a user"))
    def getDestinationIndicator(self, client_id, uid, cn_query, rotate=True):
        """

        :param client_id: UUID of the client used to find the closest destinationIndicators
        :param uid: uid of the user
        :param cn_query: filter for destinationIndicator-cns (e.g. 'lts-% for wildcards)
        :param rotate: rotate the destinationIndicators (do not use the last one twice in a row)
        :return: FQDN of the server marked as destinationIndicator
        """
        index = PluginRegistry.getInstance('ObjectIndex')
        res = index.search({'_type': 'User', 'uid': uid}, {'dn': 1})
        if len(res) == 0:
            raise ValueError(C.make_error("USER_NOT_FOUND", user=uid, status_code=404))

        user = ObjectProxy(res[0]['dn'])

        if rotate is False and user.destinationIndicator is not None:
            # nothing to rotate, take the stored one
            return user.destinationIndicator

        client = self.__open_device(client_id, read_only=True)
        parent_dn = client.get_adjusted_parent_dn()
        res = index.search({'_type': 'Device', 'extension': 'GoServer', 'cn': cn_query, '_adjusted_parent_dn': parent_dn}, {'dn': 1})

        while len(res) == 0 and len(parent_dn) > len(self.env.base):
            parent_dn = dn2str(str2dn(parent_dn, flags=ldap.DN_FORMAT_LDAPV3)[1:])
            res = index.search({'_type': 'Device', 'cn': cn_query, '_adjusted_parent_dn': parent_dn}, {'dn': 1})

        if len(res) > 0:
            di_pool = sorted([x['dn'] for x in res])
            if user.destinationIndicator is None:
                # nothing to rotate, take the first one
                user.destinationIndicator = di_pool[0]
                user.commit()

            elif rotate is True:
                if user.destinationIndicator in di_pool:
                    # take the next one from list
                    position = di_pool.index(user.destinationIndicator)+1
                    if position >= len(di_pool):
                        position = 0
                    user.destinationIndicator = di_pool[position]
                    user.commit()
                else:
                    # nothing to rotate, take the first one
                    user.destinationIndicator = di_pool[0]
                    user.commit()

            return user.destinationIndicator
        return None

    @Command(__help__=N_("Send synchronous notification message to user"), type="READONLY")
    def notifyUser(self, users, title, message, timeout=10, level='normal', icon="dialog-information"):
        """
        Send a notification request to the user client.
        """

        if icon is None:
            icon = "_no_icon_"

        if users:
            # Notify a single / group of users
            if type(users) != list:
                users = [users]

            for user in users:
                clients = self.getUserClients(user)
                if clients:
                    for client in clients:
                        try:
                            self.clientDispatch(client, "notify", user, title, message,
                                    timeout, icon)
                        #pylint: disable=W0141
                        except Exception as e:
                            import traceback
                            traceback.print_exc()
                            self.log.error("sending message failed: %s" % str(e))
                else:
                    self.log.error("sending message failed: no client found for user '%s'" % user)

                # Notify websession user if available
                if JsonRpcHandler.user_sessions_available(user):
                    mqtt = self.__get_handler()
                    mqtt.send_event(self.notification2event(user, title, message, timeout, icon), topic="%s/client/%s" % (self.env.domain, user))

        else:
            # Notify all users
            for client in self.__client.keys():
                try:
                    self.clientDispatch(client, "notify_all", title, message,
                            timeout, icon)
                #pylint: disable=W0141
                except Exception:
                    pass

            # Notify all websession users if any
            if JsonRpcHandler.user_sessions_available(None):
                mqtt = self.__get_handler()
                mqtt.send_event(self.notification2event("*", title, message, timeout, icon))

    def notification2event(self, user, title, message, timeout, icon):
        e = EventMaker()
        data = [e.Target(user)]
        if title:
            data.append(e.Title(title))
        data.append(e.Body(message))
        if timeout:
            data.append(e.Timeout(str(timeout * 1000)))
        if icon != "_no_icon_":
            data.append(e.Icon(icon))

        return e.Event(e.Notification(*data))

    def __open_device(self, device_uuid, read_only=False):
        device_uuid = self.get_client_uuid(device_uuid)
        index = PluginRegistry.getInstance("ObjectIndex")

        res = index.search({'_type': 'Device', 'deviceUUID': device_uuid},
                           {'dn': 1})
        if len(res) != 1:
            raise ValueError(C.make_error("CLIENT_NOT_FOUND", client=device_uuid, status_code=404))
        return ObjectProxy(res[0]['dn'], read_only=read_only, from_db_only=True)

    @Command(__help__=N_("Set system status"), type="READONLY")
    def systemGetStatus(self, device_uuid):
        """
        TODO
        """
        if isinstance(device_uuid, str):
            device = self.__open_device(device_uuid)
        else:
            device = device_uuid
        return device.deviceStatus

    @Command(__help__=N_("Set system status"))
    def systemSetStatus(self, device_uuid, status):
        """
        TODO
        """
        if GlobalLock.exists("scan_index"):
            # do not update state during index, clients will be polled after index is done
            return

        if isinstance(device_uuid, str):
            device = self.__open_device(device_uuid)
        else:
            device = device_uuid
        r = re.compile(r"([+-].)")
        for stat in r.findall(status):
            if stat[1] not in mapping:
                raise ValueError(C.make_error("CLIENT_STATUS_INVALID", client=device_uuid, status=stat[1]))
            setattr(device, mapping[stat[1]], stat.startswith("+"))
        device.commit()

    @Command(needsUser=True, __help__=N_("Join a client to the GOsa infrastructure."))
    def joinClient(self, user, device_uuid, mac, info=None):
        """
        TODO
        """

        index = PluginRegistry.getInstance("ObjectIndex")

        uuid_check = re.compile(r"^[0-9a-f]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$", re.IGNORECASE)
        if not uuid_check.match(device_uuid):
            raise ValueError(C.make_error("CLIENT_UUID_INVALID", uuid=device_uuid))

        # Handle info, if present
        more_info = []
        extensions = ["simpleSecurityObject", "ieee802Device"]

        if info:
            # Check string entries
            for entry in filter(lambda x: x in info, ["serialNumber", "ou", "o", "l", "description"]):

                if not re.match(r"^[\w\s]+$", info[entry]):
                    raise ValueError(C.make_error("CLIENT_DATA_INVALID", client=device_uuid, entry=entry, data=info[entry]))

                more_info.append((entry, info[entry]))

            if "ipHostNumber" in info:
                if re.match(r"^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}", info["ipHostNumber"]):
                    more_info.append(("ipHostNumber", info["ipHostNumber"]))
                    extensions.append("IpHost")
                else:
                    raise ValueError(C.make_error("CLIENT_DATA_INVALID", client=device_uuid, entry="ipHostNumber", data=info["ipHostNumber"]))

            if "hostname" in info:
                allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
                if all(allowed.match(x) for x in info["hostname"].split(".")):
                    more_info.append(("description", info["hostname"]))

            # Check desired device type if set
            if "deviceType" in info:
                if re.match(r"^(terminal|workstation|server|sipphone|switch|router|printer|scanner)$", info["deviceType"]):

                    more_info.append(("deviceType", info["deviceType"]))
                else:
                    raise ValueError(C.make_error("CLIENT_TYPE_INVALID", client=device_uuid, type=info["deviceType"]))

            # Check owner for presence
            if "owner" in info:
                # Take a look at the directory to see if there's  such an owner DN
                res = index.search({'_dn': info["owner"]}, {'_dn': 1})
                if len(res) == 0:
                    raise ValueError(C.make_error("CLIENT_OWNER_NOT_FOUND", client=device_uuid, owner=info["owner"]))
                more_info.append(("owner", info["owner"]))

        # Generate random client key
        h, key, salt = generate_random_key()

        # Take a look at the directory to see if there's already a joined client with this uuid
        res = index.search({'_type': 'Device', 'macAddress': mac, 'extension': 'RegisteredDevice'},
                           {'dn': 1})

        if len(res) > 0:
            record = ObjectProxy(res[0]['dn'])
            for ext in extensions:
                if not record.is_extended_by(ext):
                    record.extend(ext)

            if record.is_extended_by("ForemanHost") and record.otp is not None:
                record.otp = None

            record.userPassword = ["{SSHA}" + encode(h.digest() + salt).decode()]
            for k, value in more_info:
                setattr(record, k, value)
            cn = record.deviceUUID
            record.status_Online = False
            record.status_Offline = True
            record.status_InstallationInProgress = False

            record.commit()
            self.log.info("UUID '%s' joined as %s" % (device_uuid, record.dn))
        else:

            # While the client is going to be joined, generate a random uuid and an encoded join key
            cn = str(uuid4())
            device_key = encrypt_key(device_uuid.replace("-", ""), cn + key)

            # Resolve manager
            res = index.search({'_type': 'User', 'uid': user},
                               {'dn': 1})

            if len(res) != 1:
                raise GOtoException(C.make_error("USER_NOT_UNIQUE" if res else "UNKNOWN_USER", user=user))
            manager = res[0]['dn']

            # Create new machine entry
            dn = ",".join([self.env.config.get("goto.machine-rdn", default="ou=systems"), self.env.base])
            record = ObjectProxy(dn, "Device")
            record.extend("RegisteredDevice")
            for ext in extensions:
                record.extend(ext)
            record.deviceUUID = cn
            record.deviceKey = Binary(device_key)
            record.cn = "mac%s" % mac.replace(":", "")
            record.manager = manager
            record.status_Offline = True
            record.macAddress = mac.encode("ascii", "ignore")
            record.userPassword = ["{SSHA}" + encode(h.digest() + salt).decode()]
            for k, value in more_info:
                setattr(record, k, value)

            record.commit()
            self.log.info("UUID '%s' joined as %s" % (device_uuid, record.dn))

        # make sure the client has the access rights he needs
        self.applyClientRights(cn)

        return [key, cn]

    def __eventProcessor(self, topic, message):
        if message[0:1] == "{":
            # RPC response
            self.log.debug("RPC response received in channel %s: '%s'" % (topic, message))
        else:
            try:
                data = etree.fromstring(message, PluginRegistry.getEventParser())
                eventType = stripNs(data.xpath('/g:Event/*', namespaces={'g': "http://www.gonicus.de/Events"})[0].tag)
                self.log.debug("'%s' event received from local MQTT broker" % eventType)
                if hasattr(self, "_handle"+eventType):
                    func = getattr(self, "_handle" + eventType)
                    func(data)
                else:
                    self.log.debug("unhandled event %s" % eventType)
            except etree.XMLSyntaxError as e:
                self.log.error("XML parse error %s on message %s" % (e, message))

    def _handleUserSession(self, data):
        data = data.UserSession
        id = str(data.Id)
        if hasattr(data.User, 'Name'):
            users = list(map(str, data.User.Name))
            if id in self.__user_session:
                new_users = list(set.difference(set(users), set(self.__user_session[id])))
            else:
                # all users are new
                new_users = users

            self.__user_session[id] = users

            # proxied user events need no configuration as the proxy sends an extra RPC for that
            if not hasattr(data, "Proxied") or data.Proxied is False:
                for user in users:
                    self.preUserSession(id, user, skip_config=True)

                if len(new_users):
                    self.log.debug("configuring new users: %s" % new_users)
                    self.configureUsers(id, new_users)

        else:
            self.__user_session[id] = []
            self.systemSetStatus(id, "-B")

        self.log.debug("updating client '%s' user session: %s" % (id, ','.join(self.__user_session[id])))

    @Command(__help__=N_("Prepare a user session after a user has logged in"), type="PROXY")
    def preUserSession(self, client_id, user_name, skip_config=False):
        sobj = PluginRegistry.getInstance("SchedulerService")
        # delay changes, send configuration first
        if client_id not in self.__user_session:
            self.__user_session[client_id] = [user_name]
        elif user_name not in self.__user_session[client_id]:
            self.__user_session[client_id].append(user_name)

        if self.env.mode == "proxy":
            # answer config locally and proceed the write-part of the call to the GOsa backend
            if self.__current_backend_rpc is None or self.__current_backend_rpc.done() is True:
                self.log.info("calling preUserSession(%s, %s, skip_config=True) on master backend" % (client_id, user_name))
                self.__current_backend_rpc = self.__cr.dispatchRemote(client_id, None, 'preUserSession', client_id, user_name, skip_config=True)

            if skip_config is False:
                user_config = self.__collect_user_configuration(client_id, [user_name])
                if user_config is not None and user_name in user_config:
                    return user_config[user_name]

        elif skip_config is True:
            self.__maintain_user_session(client_id, user_name)
        else:
            # normal (non-proxy) mode -> return the config and schedule the user_session maintenance
            # to prevent it from blocking the config generation
            sobj.getScheduler().add_date_job(self.__maintain_user_session,
                                             datetime.datetime.now() + datetime.timedelta(milliseconds=250),
                                             args=(client_id, user_name),
                                             tag='_internal', jobstore='ram')

            user_config = self.__collect_user_configuration(client_id, [user_name])
            if user_config is not None and user_name in user_config:
                return user_config[user_name]

        return None

    def __maintain_user_session(self, client_id, user_name):
        # save login time and system<->user references
        client = self.__open_device(client_id)
        client.gotoLastUser = user_name
        self.systemSetStatus(client, "+B")

        index = PluginRegistry.getInstance("ObjectIndex")
        res = index.search({"_type": "User", "uid": user_name}, {"dn": 1})
        for u in res:
            user = ObjectProxy(u["dn"])
            if not user.is_extended_by("GosaAccount"):
                user.extend("GosaAccount")
            user.gotoLastSystemLogin = datetime.datetime.now()
            user.gotoLastSystem = client.dn
            user.commit()

    @Command(__help__=N_("Cleanup a user session after a user has logged out"))
    def postUserSession(self, client_id, user):
        """
        :param client_id: clients deviceUUID
        :param user: uid of the user that has logged out
        """
        if client_id not in self.__user_session:
            self.__user_session[client_id] = []
        if user in self.__user_session[client_id]:
            self.__user_session[client_id].remove(user)
        if len(self.__user_session[client_id]) == 0:
            self.systemSetStatus(client_id, "-B")
            return True
        return False

    @Command(__help__="Send user configurations of all logged in user to a client")
    def configureUsers(self, client_id, users):
        users_config = self.__collect_user_configuration(client_id, users)
        for uid, config in users_config.items():
            if "menu" in config:
                # send to client
                self.log.debug("sending generated menu for user %s" % uid)
                self.queuedClientDispatch(client_id, "dbus_configureUserMenu", uid, dumps(config["menu"]))

            if "printer-setup" in config:
                self.configureHostPrinters(client_id, config["printer-setup"])

            if "resolution" in config and config["resolution"] is not None and len(config["resolution"]):
                self.log.debug("sending screen resolution: %sx%s for user %s to client %s" % (config["resolution"][0], config["resolution"][1], uid, client_id))
                self.queuedClientDispatch(client_id, "dbus_configureUserScreen", uid, config["resolution"][0], config["resolution"][1])

    def __collect_user_configuration(self, client_id, users):
        """
        :param client_id: deviceUUID or hostname
        :param users: list of currently logged in users on the client
        """
        if isinstance(client_id, ObjectProxy):
            client = client_id
        else:
            client = self.__open_device(client_id, read_only=True)
        group = None
        index = PluginRegistry.getInstance("ObjectIndex")
        res = index.search({"_type": "GroupOfNames", "member": client.dn}, {"dn": 1})
        if len(res) > 0:
            group = ObjectProxy(res[0]["dn"], read_only=True)
        config = {}

        resolution = None
        if group is not None and group.is_extended_by("GotoEnvironment") and group.gotoXResolution is not None:
            resolution = group.gotoXResolution

        if client.is_extended_by("GotoEnvironment") and client.gotoXResolution is not None:
            resolution = client.gotoXResolution

        release = None
        if client.is_extended_by("GotoMenu"):
            release = client.getReleaseName()
        elif group is not None and group.is_extended_by("ForemanHostGroup"):
            release = group.getReleaseName()
            parent_group = group
            while release is None and parent_group is not None and parent_group.parent_id is not None:
                res = index.search({"_type": "GroupOfNames", "extension": "ForemanHostGroup", "foremanGroupId": parent_group.parent_id}, {"dn": 1})
                if len(res) == 0:
                    break
                else:
                    parent_group = ObjectProxy(res[0]["dn"], read_only=True)
                    release = parent_group.getReleaseName()

        if release is None:
            self.log.error("no release found for client/user combination (%s/%s)" % (client_id, users))

        client_menu = None

        if hasattr(client, "gotoMenu") and client.gotoMenu is not None:
            client_menu = loads(client.gotoMenu)

        # collect users DNs
        query_result = index.search({"_type": "User", "uid": {"in_": users}}, {"dn": 1})
        for entry in query_result:
            user = ObjectProxy(entry["dn"], read_only=True)
            config[user.uid] = {}

            if release is not None:
                menus = []
                if client_menu is not None:
                    menus.append(client_menu)

                # get all groups the user is member of which have a menu for the given release
                query = {'_type': 'GroupOfNames', "member": user.dn, "extension": "GotoMenu", "gotoLsbName": release}

                for res in index.search(query, {"gotoMenu": 1}):
                    # collect user menus
                    for m in res.get("gotoMenu", []):
                        menus.append(loads(m))

                if len(menus):
                    user_menu = None
                    for menu_entry in menus:
                        if user_menu is None:
                            user_menu = self.get_submenu(menu_entry)
                        else:
                            self.merge_submenu(user_menu, self.get_submenu(menu_entry))
                    config[user.uid]["menu"] = user_menu

            # collect printer settings for user, starting with the clients printers
            settings = self.__collect_printer_settings(group)
            printer_names = [x["cn"] for x in settings["printers"]]
            # get all GroupOfNames with GotoEnvironment the user or client is member of
            for res in index.search({'_type': 'GroupOfNames', "member": {"in_": [user.dn, client.dn]}, "extension": "GotoEnvironment"},
                                    {"dn": 1}):
                user_group = ObjectProxy(res["dn"], read_only=True)
                if group is not None and user_group.dn == group.dn:
                    # this group has already been handled
                    continue
                s = self.__collect_printer_settings(user_group)

                if user_group.gotoXResolution is not None:
                    resolution = user_group.gotoXResolution

                for p in s["printers"]:
                    if p["cn"] not in printer_names:
                        settings["printers"].append(p)
                        printer_names.append(p["cn"])

                if s["defaultPrinter"] is not None:
                    settings["defaultPrinter"] = s["defaultPrinter"]

            # override group environment settings if the client has one
            s = self.__collect_printer_settings(client)
            if len(s["printers"]) > 0:
                settings["printers"] = s["printers"]
                settings["defaultPrinter"] = s["defaultPrinter"]

            if user.is_extended_by("GosaAccount") and user.gosaDefaultPrinter is not None:
                # check if the users default printer is send to the client
                found = False
                for printer_settings in settings["printers"]:
                    if printer_settings["cn"] == user.gosaDefaultPrinter:
                        found = True
                        break

                def process(res):
                    if len(res) == 0:
                        self.log.warning("users defaultPrinter not found: %s" % user.gosaDefaultPrinter)
                        return None
                    elif len(res) == 1:
                        # add this one to the result set
                        printer = ObjectProxy(res[0]["dn"], read_only=True)
                        p_conf = {}
                        for attr in self.printer_attributes:
                            p_conf[attr] = getattr(printer, attr)
                        return p_conf
                    return False

                if found is False:
                    # find the printer and add it to the settings
                    res = index.search({"_type": "GotoPrinter", "cn": user.gosaDefaultPrinter}, {"dn": 1})
                    printer_config = process(res)
                    if printer_config is False:
                        # more than 1 printers found by this CN, try to look in the users subtree
                        res = index.search({
                            "_type": "GotoPrinter",
                            "cn": user.gosaDefaultPrinter,
                            "_adjusted_parent_dn": user.get_adjusted_parent_dn()
                        }, {"dn": 1})
                        printer_config = process(res)

                    if isinstance(printer_config, dict):
                        settings["printers"].append(printer_config)
                        settings["defaultPrinter"] = user.gosaDefaultPrinter
                    else:
                        self.log.warning("users defaultPrinter not found: %s" % user.gosaDefaultPrinter)
                else:
                    settings["defaultPrinter"] = user.gosaDefaultPrinter

            config[user.uid]["printer-setup"] = settings
            config[user.uid]["resolution"] = None

            if resolution is not None:
                config[user.uid]["resolution"] = [int(x) for x in resolution.split("x")]

            # TODO: collect and send login scripts to client
        return config

    def merge_submenu(self, menu1, menu2):
        for cn, app in menu2.get('apps', {}).items():
            if cn in menu1['apps']:
                prio1 = int(menu1[cn].get('gosaApplicationPriority', '0'))
                prio2 = int(menu2[cn].get('gosaApplicationPriority', '0'))
                if prio2 >= prio1:
                    menu1['apps'][cn] = app
            else:
                menu1['apps'][cn] = app

        for menu_entry in menu2.get('menus', {}):
            if menu_entry in menu1['menus']:
                for cn, app in menu2['menus'][menu_entry].get('apps', {}).items():
                    if cn in menu1['menus'][menu_entry]['apps']:
                        prio1 = int(menu1['menus'][menu_entry]['apps'][cn].get('gosaApplicationPriority', '0'))
                        prio2 = int(menu2['menus'][menu_entry]['apps'][cn].get('gosaApplicationPriority', '0'))
                        if prio2 >= prio1:
                            menu1['menus'][menu_entry]['apps'][cn] = app
                    else:
                        menu1['menus'][menu_entry]['apps'][cn] = app
            else:
                menu1['menus'][menu_entry] = menu2['menus'][menu_entry]

            if 'menus' in menu2['menus'][menu_entry]:
                if menu_entry in menu1['menus'] and 'menus' in menu1['menus'][menu_entry]:
                    self.merge_submenu(menu1['menus'][menu_entry], menu2['menus'][menu_entry])
                else:
                    menu1['menus'][menu_entry]['menus'] = menu2['menus'][menu_entry]['menus']

    def get_submenu(self, entries):
        result = None
        for entry in entries:
            if result is None:
                result = {'apps': {}}

            if 'children' in entry:
                if not 'menus' in result:
                    result['menus'] = {}
                result['menus'][entry.get('name', N_('unknown'))] = self.get_submenu(entry['children'])
            else:
                application = self.get_application(entry)
                result['apps'][application.get('cn', 'name')] = application

        return result

    def get_application(self, application):
        result = None
        if 'name' in application and 'dn' in application:
            result = {'name': application.get('name')}
            if 'gosaApplicationParameter' in application:
                result['gosaApplicationParameter'] = application.get('gosaApplicationParameter')

            application = ObjectProxy(application.get('dn'))
            if application is not None:
                for attribute in self.entry_attributes:
                    if hasattr(application, attribute):
                        attribute_name = self.entry_map.get(attribute, attribute)
                        result[attribute_name] = getattr(application, attribute)

        return result

    @Command(__help__=N_("Send user specific configuration (e.g. printers) to a clients active user sessions"))
    def configureClient(self, client_id):
        """
        :param client_id: deviceUUID or hostname
        """
        if client_id in self.__user_session:
            self.configureUsers(client_id, self.__user_session[client_id])
        else:
            self.log.debug("no active user found for client %s" % client_id)

    def configureHostPrinters(self, client_id, config):
        """ configure the printers for this client via dbus. """
        if "printers" not in config or len(config["printers"]) == 0:
            return
        # delete old printers first
        self.queuedClientDispatch(client_id, "dbus_deleteAllPrinters")

        for p_conf in config["printers"]:
            self.queuedClientDispatch(client_id, "dbus_addPrinter", p_conf)

        if "defaultPrinter" in config and config["defaultPrinter"] is not None:
            self.queuedClientDispatch(client_id, "dbus_defaultPrinter", config["defaultPrinter"])

    def __collect_printer_settings(self, object):
        settings = {"printers": [], "defaultPrinter": None}
        if object is not None and object.is_extended_by("GotoEnvironment") and len(object.gotoPrinters):
            # get default printer
            settings["defaultPrinter"] = object.gotoDefaultPrinter

            # collect printer PPDs
            for printer_dn in object.gotoPrinters:
                printer = ObjectProxy(printer_dn, read_only=True)
                p_conf = {}
                for attr in self.printer_attributes:
                    p_conf[attr] = getattr(printer, attr) if getattr(printer, attr) is not None else ""
                    if attr == "gotoPrinterPPD" and p_conf[attr] != "":
                        p_conf[attr] = self.ppd_proxy.getPPDURL(p_conf[attr])

                settings["printers"].append(p_conf)
        return settings

    def _handleClientPing(self, data):
        data = data.ClientPing
        client = data.Id.text
        if GlobalLock.exists("scan_index"):
            # ignore ping event
            self.log.info("ignoring ping from client '%s' during indexing" % client)
            return

        self.__set_client_online(client)
        if client in self.__client:
            self.__client[client]['last-seen'] = datetime.datetime.utcnow()

    def _handleClientSignature(self, data):
        data = data.ClientSignature
        client = data.Id.text
        self.log.info("client '%s' has an signature update for us" % client)

        # Remove remaining proxy values for this client
        if client in self.__proxy:
            self.__proxy[client].close()
            del self.__proxy[client]

        # Assemble caps
        caps = {}
        for method in data.ClientCapabilities.ClientMethod:
            self.log.debug("client %s provides method %s" % (client, method.Name.text))
            caps[method.Name.text] = {
                'path': method.Path.text,
                'sig': method.Signature.text,
                'doc': method.Documentation.text}

        # This may happen if we get a stuck event
        if not data.Id.text in self.__client:
            return

        # Decide if we need to notify someone about new methods
        current = copy(self.__client[data.Id.text]['caps'])
        self.__client[data.Id.text]['caps'] = caps
        for method in [m for m in current.keys() if not m in caps]:
            self.notify_listeners(data.Id.text, method, False)
        for method in [m for m in caps if not m in current.keys()]:
            self.notify_listeners(data.Id.text, method, True)

    def notify_listeners(self, cid, method, status):
        if method in self.__listeners:
            for cb in self.__listeners[method]:
                cb(cid, method, status)

    def register_listener(self, method, callback):
        if not method in self.__listeners:
            self.__listeners[method] = []

        if not callback in self.__listeners[method]:
            self.__listeners[method].append(callback)

    def unregister_listener(self, method, callback):
        if not method in self.__listeners:
            return

        if not callback in self.__listeners[method]:
            return

        self.__listeners[method].pop(self.__listeners[method].index(callback))

    def _handleClientAnnounce(self, data):
        data = data.ClientAnnounce
        client = data.Id.text
        self.log.info("client '%s' is joining us" % client)
        self.systemSetStatus(client, "+O-o")

        # Assemble network information
        network = {}
        for interface in data.NetworkInformation.NetworkDevice:
            network[interface.Name.text] = {
                'IPAddress': interface.IPAddress.text,
                'IPv6Address': interface.IPv6Address.text,
                'MAC': interface.MAC.text,
                'Netmask': interface.Netmask.text,
                'Broadcast': interface.Broadcast.text}

        # Add recieve time to be able to sort out dead nodes
        t = datetime.datetime.utcnow()
        info = {
            'name': data.Name.text,
            'last-seen': t,
            'online': True,
            'caps': {},
            'network': network
        }

        self.__client[client] = info

    def _on_client_caps(self, cid, method, status):
        if status is False:
            return

        self.log.debug("client %s provides method %s" % (cid, method))
        if cid in self.__client_call_queue and method in self.__client_call_queue[cid]:
            for arg, larg in self.__client_call_queue[cid][method]:
                self.clientDispatch(cid, method, *arg, **larg)
            del self.__client_call_queue[cid][method]

            if len(self.__client_call_queue[cid].keys()) == 0:
                del self.__client_call_queue[cid]

            # check if we still have listeners for that method
            delete = True
            for client_id, queue in self.__client_call_queue.items():
                if method in queue:
                    delete = False
                    break

            if delete is True:
                self.unregister_listener(method, self._on_client_caps)

    def _handleClientLeave(self, data):
        data = data.ClientLeave
        client = data.Id.text
        self.log.info("client '%s' is leaving" % client)
        self.__set_client_offline(client, True)

    def __set_client_online(self, client):
        self.systemSetStatus(client, "+O-o")
        if client in self.__client:
            self.__client[client]['online'] = True

    def __set_client_offline(self, client, purge=False):
        try:
            self.systemSetStatus(client, "-O+o")
        except ValueError as e:
            id = C.get_error_id(str(e))
            error = C.getError(None, None, id, keep=True)
            if error['status_code'] == 404:
                pass
            else:
                raise e

        if client in self.__client:
            if purge:
                del self.__client[client]

                if client in self.__proxy:
                    self.__proxy[client].close()
                    del self.__proxy[client]

                if client in self.__user_session:
                    del self.__user_session[client]

            else:
                self.__client[client]['online'] = False

    def __gc(self):
        interval = int(self.env.config.get("goto.timeout", default="600"))

        for client, info in self.__client.items():
            if not info['online']:
                continue

            if info['last-seen'] < datetime.datetime.utcnow() - datetime.timedelta(seconds=2 * interval):
                self.log.info("client '%s' looks dead - setting to 'offline'" % client)
                self.__set_client_offline(client)

    def applyClientRights(self, device_uuid):
        # check rights
        acl = PluginRegistry.getInstance("ACLResolver")
        allowed_commands = [
            'joinClient',
            'preUserSession',
            'postUserSession',
            'getMethods',
            'getDestinationIndicator'
        ]
        missing = [x for x in allowed_commands if not acl.check(device_uuid, "%s.%s.%s" % (self.env.domain, "command", x), "x")]
        reload = False
        role_name = "$$ClientDevices"

        if len(missing) > 0:
            # create AclRole for joining if not exists
            index = PluginRegistry.getInstance("ObjectIndex")
            res = index.search({"_type": "AclRole", "name": role_name}, {"dn": 1})
            if len(res) == 0:
                # create
                role = ObjectProxy(self.env.base, "AclRole")
                role.name = role_name
            else:
                role = ObjectProxy(res[0]['dn'])

            # create rule
            aclentry = {
                "priority": 0,
                "scope": "sub",
                "actions": [
                    {
                        "topic": "%s\.command\.(%s)" % (self.env.domain, "|".join(allowed_commands)),
                        "acl": "x",
                        "options": {}
                    }
                ]}
            role.AclRoles = [aclentry]
            role.commit()
            reload = True

        # check if device has role
        found = False
        base = ObjectProxy(self.env.base)
        if base.is_extended_by("Acl"):
            for entry in base.AclSets:
                if entry["rolename"] == role_name and device_uuid in entry["members"]:
                    found = True
                    break
        else:
            base.extend("Acl")

        if found is False:
            acl_entry = {"priority": 0,
                         "members": [device_uuid],
                         "rolename": role_name}
            base.AclSets.append(acl_entry)
            if self.env.mode != "proxy":
                self.__acl_change_checks.append({"role": role_name, "member": device_uuid})
            base.commit()
            reload = True

        if reload is True:
            # reload acls to make sure that they are applied in the current instance
            acl.load_acls()
Ejemplo n.º 10
0
 def __get_handler(self):
     if self.mqtt is None:
         self.mqtt = MQTTHandler(client_id_prefix="ClientService")
     return self.mqtt
Ejemplo n.º 11
0
 def __get_handler(self):
     if self.mqtt is None:
         self.mqtt = MQTTHandler()
     return self.mqtt
Ejemplo n.º 12
0
class ClientService(Plugin):
    """
    Plugin to register clients and expose their functionality
    to the users.

    Keys for configuration section **goto**

    +------------------+------------+-------------------------------------------------------------+
    + Key              | Format     +  Description                                                |
    +==================+============+=============================================================+
    + machine-rdn      | String     + RDN to initially place new machines in.                     |
    +------------------+------------+-------------------------------------------------------------+
    + timeout          | Integer    + Client ping interval.                                       |
    +------------------+------------+-------------------------------------------------------------+

    """
    
    _priority_ = 90
    _target_ = 'goto'
    __client = {}
    __proxy = {}
    __user_session = {}
    __listeners = {}

    def __init__(self):
        """
        Construct a new ClientService instance based on the configuration
        stored in the environment.
        """
        env = Environment.getInstance()
        self.log = logging.getLogger(__name__)
        self.log.info("initializing client service")
        self.env = env
        self.__cr = None
        self.mqtt = None

    def __get_handler(self):
        if self.mqtt is None:
            self.mqtt = MQTTHandler()
        return self.mqtt

    def serve(self):
        # Add event processor
        mqtt = self.__get_handler()
        mqtt.get_client().add_subscription('%s/client/+' % self.env.domain)
        self.log.debug("subscribing to %s event queue" % '%s/client/+' % self.env.domain)
        mqtt.set_subscription_callback(self.__eventProcessor)

        # Get registry - we need it later on
        self.__cr = PluginRegistry.getInstance("CommandRegistry")

        # Start maintenance with a delay of 5 seconds
        timer = Timer(5.0, self.__refresh)
        timer.start()
        self.env.threads.append(timer)

        # Register scheduler task to remove outdated clients
        sched = PluginRegistry.getInstance('SchedulerService').getScheduler()
        sched.add_interval_job(self.__gc, minutes=1, tag='_internal', jobstore="ram")

    def __refresh(self):
        # Initially check if we need to ask for client caps
        if not self.__client:
            e = EventMaker()
            self.mqtt.send_event(e.Event(e.ClientPoll()), "%s/client/broadcast" % self.env.domain)

    def stop(self):  # pragma: nocover
        pass

    @Command(__help__=N_("List available clients."))
    def getClients(self):
        """
        List available domain clients.

        ``Return:`` dict with name and timestamp information, indexed by UUID
        """
        res = {}
        for uuid, info in self.__client.items():
            if info['online']:
                res[uuid] = {'name': info['name'], 'last-seen': info['last-seen']}
        return res

    @gen.coroutine
    @Command(__help__=N_("Call method exposed by client."))
    def clientDispatch(self, client, method, *arg, **larg):
        """
        Dispatch a method on the client.

        ========= ================================
        Parameter Description
        ========= ================================
        client    Device UUID of the client
        method    Method name to call
        *         Method arguments
        ========= ================================

        ``Return:`` varies
        """

        # Bail out if the client is not available
        if not client in self.__client:
            raise JSONRPCException("client '%s' not available" % client)
        if not self.__client[client]['online']:
            raise JSONRPCException("client '%s' is offline" % client)
        if not method in self.__client[client]['caps']:
            raise JSONRPCException("client '%s' has no method '%s' exported" % (client, method))

        # Generate tag queue name
        queue = '%s/client/%s' % (self.env.domain, client)
        self.log.debug("got client dispatch: '%s(%s)', sending to %s" % (method, arg, queue))

        # client queue -> mqtt rpc proxy
        if not client in self.__proxy:
            self.__proxy[client] = MQTTServiceProxy(mqttHandler=self.mqtt, serviceAddress=queue)

        # Call her to the moon...
        methodCall = getattr(self.__proxy[client], method)

        # Do the call
        res = yield methodCall(*arg, **larg)
        raise gen.Return(res)

    @Command(__help__=N_("Get the client Interface/IP/Netmask/Broadcast/MAC list."))
    def getClientNetInfo(self, client):
        """
        Get brief information about the client network setup.

        Example:

        .. doctest::

            >>> getClientNetInfo("eb5e72d4-c53f-4612-81a3-602b14a8da69")
            {'eth0': {
                'Broadcast': '10.89.1.255',
                'MAC': '00:01:6c:9d:b9:fa',
                'IPAddress': '10.89.1.31',
                'Netmask': '255.255.255.0',
                'IPv6Address': 'fe80::201:6cff:fe9d:b9fa/64'}}

        ``Return:`` dict with network information
        """

        if not client in self.__client:
            return []

        res = self.__client[client]['network']
        return res

    @Command(__help__=N_("List available client methods for specified client."))
    def getClientMethods(self, client):
        """
        Get list of available client methods and their signature.

        ``Return:`` dict of client methods
        """
        if not client in self.__client:
            return []
        if not self.__client[client]['online']:
            return []

        return self.__client[client]['caps']

    @Command(__help__=N_("List user sessions per client"))
    def getUserSessions(self, client=None):
        """
        TODO
        """
        if client:
            return list(self.__user_session[client]) if client in self.__user_session else []

        return list(self.__user_session)

    @Command(__help__=N_("List clients a user is logged in"))
    def getUserClients(self, user):
        """
        TODO
        """
        return [client for client, users in self.__user_session.items() if user in users]

    @Command(__help__=N_("Send synchronous notification message to user"))
    def notifyUser(self, users, title, message, timeout=10, level='normal', icon="dialog-information"):
        """
        Send a notification request to the user client.
        """

        if icon is None:
            icon = "_no_icon_"

        if users:
            # Notify a single / group of users
            if type(users) != list:
                users = [users]

            for user in users:
                clients = self.getUserClients(user)
                if clients:
                    for client in clients:
                        try:
                            self.clientDispatch(client, "notify", user, title, message,
                                    timeout, icon)
                        #pylint: disable=W0141
                        except Exception as e:
                            import traceback
                            traceback.print_exc()
                            self.log.error("sending message failed: %s" % str(e))
                else:
                    self.log.error("sending message failed: no client found for user '%s'" % user)

                # Notify websession user if available
                if JsonRpcHandler.user_sessions_available(user):
                    mqtt = self.__get_handler()
                    mqtt.send_event(self.notification2event(user, title, message, timeout, icon), topic="%s/client/%s" % (self.env.domain, user))

        else:
            # Notify all users
            for client in self.__client.keys():
                try:
                    self.clientDispatch(client, "notify_all", title, message,
                            timeout, icon)
                #pylint: disable=W0141
                except Exception:
                    pass

            # Notify all websession users if any
            if JsonRpcHandler.user_sessions_available(None):
                mqtt = self.__get_handler()
                mqtt.send_event(self.notification2event("*", title, message, timeout, icon))

    def notification2event(self, user, title, message, timeout, icon):
        e = EventMaker()
        data = [e.Target(user)]
        if title:
            data.append(e.Title(title))
        data.append(e.Body(message))
        if timeout:
            data.append(e.Timeout(str(timeout * 1000)))
        if icon != "_no_icon_":
            data.append(e.Icon(icon))

        return e.Event(e.Notification(*data))

    @Command(__help__=N_("Set system status"))
    def systemGetStatus(self, device_uuid):
        """
        TODO
        """

        index = PluginRegistry.getInstance("ObjectIndex")

        res = index.search({'_type': 'Device', 'deviceUUID': device_uuid},
                           {'_uuid': 1})
        if len(res) != 1:
            raise ValueError(C.make_error("CLIENT_NOT_FOUND", device_uuid))

        device = ObjectProxy(res[0]['_uuid'])
        return device.deviceStatus

    @Command(__help__=N_("Set system status"))
    def systemSetStatus(self, device_uuid, status):
        """
        TODO
        """

        # Write to backend
        index = PluginRegistry.getInstance("ObjectIndex")

        res = index.search({'_type': 'Device', 'deviceUUID': device_uuid}, {'_uuid': 1})
        if len(res) != 1:
            raise ValueError(C.make_error("CLIENT_NOT_FOUND", device_uuid))
        device = ObjectProxy(res[0]['_uuid'])
        r = re.compile(r"([+-].)")
        for stat in r.findall(status):
            if stat[1] not in mapping:
                raise ValueError(C.make_error("CLIENT_STATUS_INVALID", device_uuid, status=stat[1]))
            setattr(device, mapping[stat[1]], stat.startswith("+"))
        device.commit()

    @Command(needsUser=True, __help__=N_("Join a client to the GOsa infrastructure."))
    def joinClient(self, user, device_uuid, mac, info=None):
        """
        TODO
        """

        index = PluginRegistry.getInstance("ObjectIndex")

        uuid_check = re.compile(r"^[0-9a-f]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$", re.IGNORECASE)
        if not uuid_check.match(device_uuid):
            raise ValueError(C.make_error("CLIENT_UUID_INVALID", device_uuid))

        # Handle info, if present
        more_info = []

        if info:
            # Check string entries
            for entry in filter(lambda x: x in info, ["serialNumber", "ou", "o", "l", "description"]):

                if not re.match(r"^[\w\s]+$", info[entry]):
                    raise ValueError(C.make_error("CLIENT_DATA_INVALID", device_uuid, entry=entry, data=info[entry]))

                more_info.append((entry, info[entry]))

            # Check desired device type if set
            if "deviceType" in info:
                if re.match(r"^(terminal|workstation|server|sipphone|switch|router|printer|scanner)$", info["deviceType"]):

                    more_info.append(("deviceType", info["deviceType"]))
                else:
                    raise ValueError(C.make_error("CLIENT_TYPE_INVALID", device_uuid, type=info["deviceType"]))

            # Check owner for presence
            if "owner" in info:
                # Take a look at the directory to see if there's  such an owner DN
                res = index.search({'_dn': info["owner"]}, {'_dn': 1})
                if len(res) == 0:
                    raise ValueError(C.make_error("CLIENT_OWNER_NOT_FOUND", device_uuid, owner=info["owner"]))
                more_info.append(("owner", info["owner"]))

        # Generate random client key
        random.seed()
        key = ''.join(random.Random().sample(string.ascii_letters + string.digits, 32))
        salt = os.urandom(4)
        h = hashlib.sha1(key.encode('ascii'))
        h.update(salt)

        # Take a look at the directory to see if there's already a joined client with this uuid
        res = index.search({'_type': 'Device', 'macAddress': mac},
                           {'_uuid': 1})

        if len(res):
            raise GOtoException(C.make_error("DEVICE_EXISTS", mac))

        # While the client is going to be joined, generate a random uuid and an encoded join key
        cn = str(uuid4())
        device_key = self.__encrypt_key(device_uuid.replace("-", ""), cn + key)

        # Resolve manager
        res = index.search({'_type': 'User', 'uid': user},
                           {'dn': 1})

        if len(res) != 1:
            raise GOtoException(C.make_error("USER_NOT_UNIQUE" if res else "UNKNOWN_USER", target=user))
        manager = res[0]['dn']

        # Create new machine entry
        # dn = ",".join([self.env.config.get("goto.machine-rdn", default="ou=systems"), self.env.base])
        # container = ObjectProxy(dn, "DeviceContainer")
        # container.commit()
        dn = ",".join([self.env.config.get("goto.machine-rdn", default="ou=devices,ou=systems"), self.env.base])
        record = ObjectProxy(dn, "Device")
        record.extend("RegisteredDevice")
        record.extend("ieee802Device")
        record.extend("simpleSecurityObject")
        record.deviceUUID = cn
        record.deviceKey = Binary(device_key)
        record.cn = cn
        record.manager = manager
        record.status_Offline = True
        record.macAddress = mac.encode("ascii", "ignore")
        record.userPassword = "******" + encode(h.digest() + salt).decode()
        for key, value in more_info:
            setattr(record, key, value)

        record.commit()
        self.log.info("UUID '%s' joined as %s" % (device_uuid, record.dn))

        return [key, cn]

        return None

    def __encrypt_key(self, key, data):
        """
        Encrypt a data using key
        """

        # Calculate padding length
        key_pad = AES.block_size - len(key) % AES.block_size
        data_pad = AES.block_size - len(data) % AES.block_size

        # Pad data PKCS12 style
        if key_pad != AES.block_size:
            key += chr(key_pad) * key_pad
        if data_pad != AES.block_size:
            data += chr(data_pad) * data_pad

        return AES.new(key, AES.MODE_ECB).encrypt(data)

    def __eventProcessor(self, topic, message):
        if message[0:1] == "{":
            # RPC response
            self.log.debug("RPC response received in channel %s: '%s'" % (topic, message))
        else:
            try:
                data = etree.fromstring(message, PluginRegistry.getEventParser())
                eventType = stripNs(data.xpath('/g:Event/*', namespaces={'g': "http://www.gonicus.de/Events"})[0].tag)
                self.log.debug("Incoming MQTT event[%s]: '%s'" % (eventType, data))
                if hasattr(self, "_handle"+eventType):
                    func = getattr(self, "_handle" + eventType)
                    func(data)
                else:
                    self.log.debug("unhandled event %s" % eventType)
            except etree.XMLSyntaxError as e:
                self.log.error("XML parse error %s on message %s" % (e, message))

    def _handleUserSession(self, data):
        data = data.UserSession
        if hasattr(data.User, 'Name'):
            self.__user_session[str(data.Id)] = list(map(str, data.User.Name))
            self.systemSetStatus(str(data.Id), "+B")
        else:
            self.__user_session[str(data.Id)] = []
            self.systemSetStatus(str(data.Id), "-B")

        self.log.debug("updating client '%s' user session: %s" % (data.Id,
                ','.join(self.__user_session[str(data.Id)])))

    def _handleClientPing(self, data):
        data = data.ClientPing
        client = data.Id.text
        self.__set_client_online(data.Id.text)
        if client in self.__client:
            self.__client[client]['last-seen'] = datetime.datetime.utcnow()

    def _handleClientSignature(self, data):
        data = data.ClientSignature
        client = data.Id.text
        self.log.info("client '%s' has an signature update for us" % client)

        # Remove remaining proxy values for this client
        if client in self.__proxy:
            self.__proxy[client].close()
            del self.__proxy[client]

        # Assemble caps
        caps = {}
        for method in data.ClientCapabilities.ClientMethod:
            caps[method.Name.text] = {
                'path': method.Path.text,
                'sig': method.Signature.text,
                'doc': method.Documentation.text}

        # This may happen if we get a stuck event
        if not data.Id.text in self.__client:
            return

        # Decide if we need to notify someone about new methods
        current = copy(self.__client[data.Id.text]['caps'])
        self.__client[data.Id.text]['caps'] = caps
        for method in [m for m in current.keys() if not m in caps]:
            self.notify_listeners(data.Id.text, method, False)
        for method in [m for m in caps if not m in current.keys()]:
            self.notify_listeners(data.Id.text, method, True)

    def notify_listeners(self, cid, method, status):
        if method in self.__listeners:
            for cb in self.__listeners[method]:
                cb(cid, method, status)

    def register_listener(self, method, callback):
        if not method in self.__listeners:
            self.__listeners[method] = []

        if not callback in self.__listeners[method]:
            self.__listeners[method].append(callback)

    def unregister_listener(self, method, callback):
        if not method in self.__listeners:
            return

        if not callback in self.__listeners[method]:
            return

        self.__listeners[method].pop(self.__listeners[method].index(callback))

    def _handleClientAnnounce(self, data):
        data = data.ClientAnnounce
        client = data.Id.text
        self.log.info("client '%s' is joining us" % client)
        self.systemSetStatus(client, "+O-o")

        # Assemble network information
        network = {}
        for interface in data.NetworkInformation.NetworkDevice:
            network[interface.Name.text] = {
                'IPAddress': interface.IPAddress.text,
                'IPv6Address': interface.IPv6Address.text,
                'MAC': interface.MAC.text,
                'Netmask': interface.Netmask.text,
                'Broadcast': interface.Broadcast.text}

        # Add recieve time to be able to sort out dead nodes
        t = datetime.datetime.utcnow()
        info = {
            'name': data.Name.text,
            'last-seen': t,
            'online': True,
            'caps': {},
            'network': network
        }

        self.__client[data.Id.text] = info

        # Handle pending "P"repare actions for that client
        if "P" in self.systemGetStatus(client):
            try:
                rm = PluginRegistry.getInstance("RepositoryManager")
                rm.prepareClient(client)
            except ValueError:
                pass

    def _handleClientLeave(self, data):
        data = data.ClientLeave
        client = data.Id.text
        self.log.info("client '%s' is leaving" % client)
        self.__set_client_offline(client, True)

    def __set_client_online(self, client):
        self.systemSetStatus(client, "+O-o")
        if client in self.__client:
            self.__client[client]['online'] = True

    def __set_client_offline(self, client, purge=False):
        self.systemSetStatus(client, "-O+o")

        if client in self.__client:
            if purge:
                del self.__client[client]

                if client in self.__proxy:
                    self.__proxy[client].close()
                    del self.__proxy[client]

                if client in self.__user_session:
                    del self.__user_session[client]

            else:
                self.__client[client]['online'] = False

    def __gc(self):
        interval = int(self.env.config.get("goto.timeout", default="600"))

        for client, info in self.__client.items():
            if not info['online']:
                continue

            if info['last-seen'] < datetime.datetime.utcnow() - datetime.timedelta(seconds=2 * interval):
                self.log.info("client '%s' looks dead - setting to 'offline'" % client)
                self.__set_client_offline(client)
Ejemplo n.º 13
0
class MQTTRelayService(object):
    """
     This service acts as a proxy between the backend and proxy MQTT brokers
     to forward messages from one to the other.

     In detail this service listens to (event-)messages from the backend to the clients on the backends MQTT broker
     and forwards then to the clients (via the proxies MQTT broker) and the other way around.

     In addition to that this service also handles events sent from the backend to the proxy (those are not forwarded
     to the clients)
    """

    _priority_ = 10
    backend_mqtt = None
    proxy_mqtt = None

    def __init__(self):
        self.env = Environment.getInstance()
        self.log = logging.getLogger(__name__)

    def serve(self):
        self.backend_mqtt = MQTTHandler(
            host=self.env.config.get("backend.mqtt-host"),
            port=self.env.config.getint("backend.mqtt-port", default=1883))

        # subscribe to all client relevant topics
        self.backend_mqtt.get_client().add_subscription("%s/client/#" % self.env.domain, qos=1)
        # subscribe to proxy topic
        self.backend_mqtt.get_client().add_subscription("%s/proxy" % self.env.domain, qos=1)
        self.backend_mqtt.set_subscription_callback(self._handle_backend_message)

        # set our last will and testament
        e = EventMaker()
        goodbye = e.Event(e.ClientLeave(e.Id(self.env.core_uuid)))
        self.backend_mqtt.will_set("%s/proxy" % self.env.domain, goodbye, qos=1)

        # connect to the proxy MQTT broker (where the clients are listening)
        self.proxy_mqtt = MQTTHandler(
            host=self.env.config.get("mqtt.host"),
            port=self.env.config.getint("mqtt.port", default=1883))
        self.proxy_mqtt.get_client().add_subscription("%s/client/#" % self.env.domain, qos=1)
        self.proxy_mqtt.set_subscription_callback(self._handle_proxy_message)

    def _handle_backend_message(self, topic, message):
        """ forwards backend messages to proxy MQTT and handles received events"""

        forward = not topic.startswith("%s/proxy" % self.env.domain)
        if message[0:1] != "{":
            # event received
            try:
                xml = objectify.fromstring(message)
                if hasattr(xml, "ClientPoll"):
                    self.__handleClientPoll()
                elif hasattr(xml, "Trigger"):
                    if xml.Trigger.Type == "ACLChanged":
                        self.log.debug("ACLChanged trigger received, reloading ACLs")
                        resolver = PluginRegistry.getInstance("ACLResolver")
                        resolver.load_acls()
                    else:
                        self.log.warning("unhandled Trigger event of type: %s received" % xml.Trigger.Type)

            except etree.XMLSyntaxError as e:
                self.log.error("Message parsing error: %s" % e)

        if forward is True:
            self.proxy_mqtt.send_message(message, topic, qos=1)

    def _handle_proxy_message(self, topic, message):
        """ forwards backend messages to proxy MQTT """
        self.backend_mqtt.send_message(message, topic, qos=1)

    def __handleClientPoll(self):
        """ register proxy-backend again """
        index = PluginRegistry.getInstance("ObjectIndex")
        index.registerProxy()

    def close(self):
        self.backend_mqtt.close()
        self.proxy_mqtt.close()

    def stop(self):
        self.close()
        self.backend_mqtt = None
        self.proxy_mqtt = None
Ejemplo n.º 14
0
class MQTTRPCService(object):
    """
    The ProxyServer handles to the RPC calls from the GOsa proxies received via MQTT
    """
    mqtt = None
    _priority_ = 10
    __command_registry = None

    def __init__(self):
        self.env = Environment.getInstance()
        self.log = logging.getLogger(__name__)
        self.subtopic = "%s/proxy" % self.env.domain
        self.mqtt = MQTTHandler(host=self.env.config.get("mqtt.host"),
                                port=self.env.config.getint("mqtt.port",
                                                            default=1883))

    def serve(self):
        self.mqtt.get_client().add_subscription('%s/#' % self.subtopic)
        self.mqtt.set_subscription_callback(self.handle_request)
        self.__command_registry = PluginRegistry.getInstance('CommandRegistry')
        self.log.info(
            "MQTT RPC service started, listening on subtopic '%s/#'" %
            self.subtopic)

    def handle_request(self, topic, message):
        if topic == self.subtopic:
            # event from proxy received
            try:
                data = etree.fromstring(message,
                                        PluginRegistry.getEventParser())
                event_type = stripNs(
                    data.xpath(
                        '/g:Event/*',
                        namespaces={'g':
                                    "http://www.gonicus.de/Events"})[0].tag)
                if event_type == "ClientLeave":
                    proxy_id = str(data.ClientLeave.Id)
                    registry = PluginRegistry.getInstance("BackendRegistry")
                    registry.unregisterBackend(proxy_id)

            except etree.XMLSyntaxError as e:
                self.log.error("Event parsing error: %s" % e)

        elif topic.startswith(self.subtopic):
            response_topic = "%s/response" % "/".join(topic.split("/")[0:4])

            try:
                id_, res = self.process(topic, message)
                response = dumps({"result": res, "id": id_})

            except Exception as e:
                err = str(e)
                self.log.error("MQTT RPC call error: %s" % err)
                response = dumps({'id': topic.split("/")[-2], 'error': err})

            # Get rid of it...
            self.mqtt.send_message(response, topic=response_topic)

        else:
            self.log.warning("unhandled topic request received: %s" % topic)

    def process(self, topic, message):

        try:
            req = loads(message)
        except ValueError as e:
            raise ValueError(C.make_error("INVALID_JSON", data=str(e)))

        try:
            id_ = req['id']
            name = req['method']
            args = req['params']
            user = req['user'] if 'user' in req else topic.split("/")[2]
            sid = req['session_id'] if 'session_id' in req else None

        except KeyError as e:
            self.log.error("KeyError: %s" % e)
            raise BadServiceRequest(message)

        self.log.debug("received call [%s] for %s: %s(%s)" %
                       (id_, topic, name, args))

        try:
            return id_, self.__command_registry.dispatch(
                user, sid, name, *args)
        except Exception as e:
            # Write exception to log
            exc_type, exc_value, exc_traceback = sys.exc_info()
            self.log.error("".join(
                traceback.format_exception(exc_type, exc_value,
                                           exc_traceback)))
            raise e
Ejemplo n.º 15
0
class MQTTRPCService(object):
    """
    The ProxyServer handles to the RPC calls from the GOsa proxies received via MQTT
    """
    mqtt = None
    _priority_ = 0
    __command_registry = None

    def __init__(self):
        self.env = Environment.getInstance()
        self.log = logging.getLogger(__name__)
        self.subtopic = "%s/proxy" % self.env.domain

    def serve(self):
        self.mqtt = MQTTHandler(host=self.env.config.get("mqtt.host"),
                                port=self.env.config.getint("mqtt.port", default=1883),
                                client_id_prefix="MQTTRPCService")

        self.__command_registry = PluginRegistry.getInstance('CommandRegistry')
        self.log.info("MQTT RPC service started, listening on subtopic '%s/#'" % self.subtopic)
        self.mqtt.get_client().add_subscription('%s/#' % self.subtopic, qos=2, callback=self.handle_request)

    @gen.coroutine
    def handle_request(self, topic, message):
        if topic == self.subtopic:
            # event from proxy received
            try:
                data = etree.fromstring(message, PluginRegistry.getEventParser())
                event_type = stripNs(data.xpath('/g:Event/*', namespaces={'g': "http://www.gonicus.de/Events"})[0].tag)
                if event_type == "ClientLeave":
                    proxy_id = str(data.ClientLeave.Id)
                    registry = PluginRegistry.getInstance("BackendRegistry")
                    registry.unregisterBackend(proxy_id)

            except etree.XMLSyntaxError as e:
                self.log.error("Event parsing error: %s" % e)

        elif topic.startswith(self.subtopic):
            response_topic = "%s/response" % "/".join(topic.split("/")[0:4])

            try:
                id_, res = self.process(topic, message)
                if is_future(res):
                    res = yield res
                response = dumps({"result": res, "id": id_})
                self.log.debug("MQTT-RPC response: %s on topic %s" % (response, topic))

            except Exception as e:
                err = str(e)
                self.log.error("MQTT RPC call error: %s" % err)
                response = dumps({'id': topic.split("/")[-2], 'error': err})

            # Get rid of it...
            self.mqtt.send_message(response, topic=response_topic, qos=2)

        else:
            self.log.warning("unhandled topic request received: %s" % topic)

    def process(self, topic, message):

        try:
            req = loads(message)
        except ValueError as e:
            raise ValueError(C.make_error("INVALID_JSON", data=str(e)))

        try:
            id_ = req['id']
            name = req['method']
            args = req['params']
            kwargs = req['kwparams']
            if 'user' in req:
                user = req['user']
            else:
                user = topic.split("/")[2]
            sid = req['session_id'] if 'session_id' in req else None

        except KeyError as e:
            self.log.error("KeyError: %s" % e)
            raise BadServiceRequest(message)

        self.log.debug("received call [%s, user=%s, session-id=%s] for %s: %s(%s,%s)" % (id_, user, sid, topic, name, args, kwargs))

        try:
            return id_, self.__command_registry.dispatch(user, sid, name, *args, **kwargs)
        except Exception as e:
            # Write exception to log
            exc_type, exc_value, exc_traceback = sys.exc_info()
            self.log.error("".join(traceback.format_exception(exc_type, exc_value, exc_traceback)))
            raise e
Ejemplo n.º 16
0
class ClientService(Plugin):
    """
    Plugin to register clients and expose their functionality
    to the users.

    Keys for configuration section **goto**

    +------------------+------------+-------------------------------------------------------------+
    + Key              | Format     +  Description                                                |
    +==================+============+=============================================================+
    + machine-rdn      | String     + RDN to initially place new machines in.                     |
    +------------------+------------+-------------------------------------------------------------+
    + timeout          | Integer    + Client ping interval.                                       |
    +------------------+------------+-------------------------------------------------------------+

    """

    _priority_ = 90
    _target_ = 'goto'
    __client = {}
    __proxy = {}
    __user_session = {}
    __listeners = {}
    entry_attributes = [
        'cn', 'description', 'gosaApplicationPriority', 'gosaApplicationIcon',
        'gosaApplicationName', 'gotoLogonScript', 'gosaApplicationFlags',
        'gosaApplicationExecute'
    ]
    entry_map = {
        "gosaApplicationPriority": "prio",
        "description": "description"
    }
    printer_attributes = [
        "gotoPrinterPPD", "labeledURI", "cn", "l", "description"
    ]
    __client_call_queue = {}
    ppd_proxy = None

    def __init__(self):
        """
        Construct a new ClientService instance based on the configuration
        stored in the environment.
        """
        env = Environment.getInstance()
        self.log = logging.getLogger(__name__)
        self.log.info("initializing client service")
        self.env = env
        self.__cr = None
        self.mqtt = None

    def __get_handler(self):
        if self.mqtt is None:
            self.mqtt = MQTTHandler()
        return self.mqtt

    def serve(self):
        # Add event processor
        mqtt = self.__get_handler()
        # listen to client topics
        mqtt.get_client().add_subscription('%s/client/+' % self.env.domain)
        self.log.debug("subscribing to %s event queue" % '%s/client/+' %
                       self.env.domain)
        mqtt.set_subscription_callback(self.__eventProcessor)

        # Get registry - we need it later on
        self.__cr = PluginRegistry.getInstance("CommandRegistry")

        # Start maintenance when index scan is finished
        zope.event.subscribers.append(self.__handle_events)

        # Register scheduler task to remove outdated clients
        sched = PluginRegistry.getInstance('SchedulerService').getScheduler()
        sched.add_interval_job(self.__gc,
                               minutes=1,
                               tag='_internal',
                               jobstore="ram")

        # self.register_listener("configureHostPrinters", self._on_client_caps)
        self.ppd_proxy = PluginRegistry.getInstance("PPDProxy")

    def __handle_events(self, event):
        """
        React on object modifications to keep active ACLs up to date.
        """
        if event.__class__.__name__ == "IndexScanFinished":
            self.__refresh()

        elif event.__class__.__name__ == "ACLChanged":
            if self.env.mode != "proxy":
                e = EventMaker()
                trigger = e.Event(e.Trigger(e.Type(event.__class__.__name__)))
                self.mqtt.send_event(trigger, "%s/proxy" % self.env.domain)

    def __refresh(self):
        # Initially check if we need to ask for client caps
        if not self.__client:
            e = EventMaker()
            self.mqtt.send_event(e.Event(e.ClientPoll()),
                                 "%s/client/broadcast" % self.env.domain)

    def stop(self):  # pragma: nocover
        pass

    def get_client_uuid(self, name_or_uuid):
        if is_uuid(name_or_uuid):
            return name_or_uuid
        else:
            # hostname used
            for uuid in self.__client:
                if self.__client[uuid]["name"] == name_or_uuid:
                    return uuid
        return name_or_uuid

    @Command(__help__=N_("List available clients."), type="READONLY")
    def getClients(self):
        """
        List available domain clients.

        ``Return:`` dict with name and timestamp information, indexed by UUID
        """
        res = {}
        for uuid, info in self.__client.items():
            if info['online']:
                res[uuid] = {
                    'name': info['name'],
                    'last-seen': info['last-seen']
                }
        return res

    @gen.coroutine
    @Command(__help__=N_("Call method exposed by client."))
    def clientDispatch(self, client, method, *arg, **larg):
        """
        Dispatch a method on the client.

        ========= ================================
        Parameter Description
        ========= ================================
        client    Device UUID of the client
        method    Method name to call
        *         Method arguments
        ========= ================================

        ``Return:`` varies
        """
        client = self.get_client_uuid(client)

        # Bail out if the client is not available
        if not client in self.__client:
            raise JSONRPCException("client '%s' not available" % client)
        if not self.__client[client]['online']:
            raise JSONRPCException("client '%s' is offline" % client)
        if not method in self.__client[client]['caps']:
            raise JSONRPCException("client '%s' has no method '%s' exported" %
                                   (client, method))

        # Generate tag queue name
        queue = '%s/client/%s' % (self.env.domain, client)
        self.log.debug("got client dispatch: '%s(%s)', sending to %s" %
                       (method, arg, queue))

        # client queue -> mqtt rpc proxy
        if not client in self.__proxy:
            self.__proxy[client] = MQTTServiceProxy(mqttHandler=self.mqtt,
                                                    serviceAddress=queue)

        # Call her to the moon...
        methodCall = getattr(self.__proxy[client], method)

        # Do the call
        res = yield methodCall(*arg, **larg)
        raise gen.Return(res)

    @Command(__help__=N_("Check if the client supports a method call"),
             type="READONLY")
    def hasCapability(self, client_id, method):
        return client_id in self.__client and method in self.__client[client_id]

    def queuedClientDispatch(self, client, method, *arg, **larg):
        client = self.get_client_uuid(client)

        # Bail out if the client is not available
        if client not in self.__client:
            raise JSONRPCException("client '%s' not available" % client)
        if not self.__client[client]['online']:
            raise JSONRPCException("client '%s' is offline" % client)
        if method not in self.__client[client]['caps']:
            # wait til method gets available
            if client not in self.__client_call_queue:
                self.__client_call_queue[client] = {}
            if method not in self.__client_call_queue[client]:
                self.__client_call_queue[client][method] = []
            if method not in self.__listeners:
                self.register_listener(method, self._on_client_caps)
            self.__client_call_queue[client][method].append((arg, larg))
        else:
            self.clientDispatch(client, method, *arg, **larg)

    @Command(
        __help__=N_("Get the client Interface/IP/Netmask/Broadcast/MAC list."),
        type="READONLY")
    def getClientNetInfo(self, client):
        """
        Get brief information about the client network setup.

        Example:

        .. doctest::

            >>> getClientNetInfo("eb5e72d4-c53f-4612-81a3-602b14a8da69")
            {'eth0': {
                'Broadcast': '10.89.1.255',
                'MAC': '00:01:6c:9d:b9:fa',
                'IPAddress': '10.89.1.31',
                'Netmask': '255.255.255.0',
                'IPv6Address': 'fe80::201:6cff:fe9d:b9fa/64'}}

        ``Return:`` dict with network information
        """
        client = self.get_client_uuid(client)
        if not client in self.__client:
            return []

        res = self.__client[client]['network']
        return res

    @Command(
        __help__=N_("List available client methods for specified client."),
        type="READONLY")
    def getClientMethods(self, client):
        """
        Get list of available client methods and their signature.

        ``Return:`` dict of client methods
        """
        client = self.get_client_uuid(client)

        if not client in self.__client:
            return []
        if not self.__client[client]['online']:
            return []

        return self.__client[client]['caps']

    @Command(__help__=N_("List user sessions per client"), type="READONLY")
    def getUserSessions(self, client=None):
        """
        TODO
        """
        if client:
            client = self.get_client_uuid(client)
            return list(self.__user_session[client]
                        ) if client in self.__user_session else []

        return list(self.__user_session)

    @Command(__help__=N_("List clients a user is logged in"), type="READONLY")
    def getUserClients(self, user):
        """
        TODO
        """
        return [
            client for client, users in self.__user_session.items()
            if user in users
        ]

    @Command(__help__=N_("Send synchronous notification message to user"),
             type="READONLY")
    def notifyUser(self,
                   users,
                   title,
                   message,
                   timeout=10,
                   level='normal',
                   icon="dialog-information"):
        """
        Send a notification request to the user client.
        """

        if icon is None:
            icon = "_no_icon_"

        if users:
            # Notify a single / group of users
            if type(users) != list:
                users = [users]

            for user in users:
                clients = self.getUserClients(user)
                if clients:
                    for client in clients:
                        try:
                            self.clientDispatch(client, "notify", user, title,
                                                message, timeout, icon)
                        #pylint: disable=W0141
                        except Exception as e:
                            import traceback
                            traceback.print_exc()
                            self.log.error("sending message failed: %s" %
                                           str(e))
                else:
                    self.log.error(
                        "sending message failed: no client found for user '%s'"
                        % user)

                # Notify websession user if available
                if JsonRpcHandler.user_sessions_available(user):
                    mqtt = self.__get_handler()
                    mqtt.send_event(
                        self.notification2event(user, title, message, timeout,
                                                icon),
                        topic="%s/client/%s" % (self.env.domain, user))

        else:
            # Notify all users
            for client in self.__client.keys():
                try:
                    self.clientDispatch(client, "notify_all", title, message,
                                        timeout, icon)
                #pylint: disable=W0141
                except Exception:
                    pass

            # Notify all websession users if any
            if JsonRpcHandler.user_sessions_available(None):
                mqtt = self.__get_handler()
                mqtt.send_event(
                    self.notification2event("*", title, message, timeout,
                                            icon))

    def notification2event(self, user, title, message, timeout, icon):
        e = EventMaker()
        data = [e.Target(user)]
        if title:
            data.append(e.Title(title))
        data.append(e.Body(message))
        if timeout:
            data.append(e.Timeout(str(timeout * 1000)))
        if icon != "_no_icon_":
            data.append(e.Icon(icon))

        return e.Event(e.Notification(*data))

    def __open_device(self, device_uuid):
        device_uuid = self.get_client_uuid(device_uuid)
        index = PluginRegistry.getInstance("ObjectIndex")

        res = index.search({
            '_type': 'Device',
            'deviceUUID': device_uuid
        }, {'dn': 1})
        if len(res) != 1:
            raise ValueError(
                C.make_error("CLIENT_NOT_FOUND", device_uuid, status_code=404))
        return ObjectProxy(res[0]['dn'])

    @Command(__help__=N_("Set system status"), type="READONLY")
    def systemGetStatus(self, device_uuid):
        """
        TODO
        """
        if isinstance(device_uuid, str):
            device = self.__open_device(device_uuid)
        else:
            device = device_uuid
        return device.deviceStatus

    @Command(__help__=N_("Set system status"))
    def systemSetStatus(self, device_uuid, status):
        """
        TODO
        """
        if GlobalLock.exists("scan_index"):
            # do not update state during index, clients will be polled after index is done
            return

        if isinstance(device_uuid, str):
            device = self.__open_device(device_uuid)
        else:
            device = device_uuid
        r = re.compile(r"([+-].)")
        for stat in r.findall(status):
            if stat[1] not in mapping:
                raise ValueError(
                    C.make_error("CLIENT_STATUS_INVALID",
                                 device_uuid,
                                 status=stat[1]))
            setattr(device, mapping[stat[1]], stat.startswith("+"))
        device.commit()

    @Command(needsUser=True,
             __help__=N_("Join a client to the GOsa infrastructure."))
    def joinClient(self, user, device_uuid, mac, info=None):
        """
        TODO
        """

        index = PluginRegistry.getInstance("ObjectIndex")

        uuid_check = re.compile(
            r"^[0-9a-f]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$",
            re.IGNORECASE)
        if not uuid_check.match(device_uuid):
            raise ValueError(C.make_error("CLIENT_UUID_INVALID", device_uuid))

        # Handle info, if present
        more_info = []

        if info:
            # Check string entries
            for entry in filter(
                    lambda x: x in info,
                ["serialNumber", "ou", "o", "l", "description"]):

                if not re.match(r"^[\w\s]+$", info[entry]):
                    raise ValueError(
                        C.make_error("CLIENT_DATA_INVALID",
                                     device_uuid,
                                     entry=entry,
                                     data=info[entry]))

                more_info.append((entry, info[entry]))

            # Check desired device type if set
            if "deviceType" in info:
                if re.match(
                        r"^(terminal|workstation|server|sipphone|switch|router|printer|scanner)$",
                        info["deviceType"]):

                    more_info.append(("deviceType", info["deviceType"]))
                else:
                    raise ValueError(
                        C.make_error("CLIENT_TYPE_INVALID",
                                     device_uuid,
                                     type=info["deviceType"]))

            # Check owner for presence
            if "owner" in info:
                # Take a look at the directory to see if there's  such an owner DN
                res = index.search({'_dn': info["owner"]}, {'_dn': 1})
                if len(res) == 0:
                    raise ValueError(
                        C.make_error("CLIENT_OWNER_NOT_FOUND",
                                     device_uuid,
                                     owner=info["owner"]))
                more_info.append(("owner", info["owner"]))

        # Generate random client key
        h, key, salt = generate_random_key()

        # Take a look at the directory to see if there's already a joined client with this uuid
        res = index.search(
            {
                '_type': 'Device',
                'macAddress': mac,
                'extension': 'RegisteredDevice'
            }, {'dn': 1})

        if len(res) > 0:
            record = ObjectProxy(res[0]['dn'])
            for ext in ["simpleSecurityObject", "ieee802Device"]:
                if not record.is_extended_by(ext):
                    record.extend(ext)

            if record.is_extended_by("ForemanHost") and record.otp is not None:
                record.otp = None

            record.userPassword = [
                "{SSHA}" + encode(h.digest() + salt).decode()
            ]
            for k, value in more_info:
                setattr(record, k, value)
            cn = record.deviceUUID
            record.status_Online = False
            record.status_Offline = True
            record.status_InstallationInProgress = False

            record.commit()
            self.log.info("UUID '%s' joined as %s" % (device_uuid, record.dn))
        else:

            # While the client is going to be joined, generate a random uuid and an encoded join key
            cn = str(uuid4())
            device_key = encrypt_key(device_uuid.replace("-", ""), cn + key)

            # Resolve manager
            res = index.search({'_type': 'User', 'uid': user}, {'dn': 1})

            if len(res) != 1:
                raise GOtoException(
                    C.make_error("USER_NOT_UNIQUE" if res else "UNKNOWN_USER",
                                 target=user))
            manager = res[0]['dn']

            # Create new machine entry
            dn = ",".join([
                self.env.config.get("goto.machine-rdn", default="ou=systems"),
                self.env.base
            ])
            record = ObjectProxy(dn, "Device")
            record.extend("RegisteredDevice")
            record.extend("ieee802Device")
            record.extend("simpleSecurityObject")
            record.deviceUUID = cn
            record.deviceKey = Binary(device_key)
            record.cn = "mac%s" % mac.replace(":", "")
            record.manager = manager
            record.status_Offline = True
            record.macAddress = mac.encode("ascii", "ignore")
            record.userPassword = [
                "{SSHA}" + encode(h.digest() + salt).decode()
            ]
            for k, value in more_info:
                setattr(record, k, value)

            record.commit()
            self.log.info("UUID '%s' joined as %s" % (device_uuid, record.dn))

        # make sure the client has the access rights he needs
        self.applyClientRights(cn)

        return [key, cn]

    def __eventProcessor(self, topic, message):
        if message[0:1] == "{":
            # RPC response
            self.log.debug("RPC response received in channel %s: '%s'" %
                           (topic, message))
        else:
            try:
                data = etree.fromstring(message,
                                        PluginRegistry.getEventParser())
                eventType = stripNs(
                    data.xpath(
                        '/g:Event/*',
                        namespaces={'g':
                                    "http://www.gonicus.de/Events"})[0].tag)
                if hasattr(self, "_handle" + eventType):
                    func = getattr(self, "_handle" + eventType)
                    func(data)
                else:
                    self.log.debug("unhandled event %s" % eventType)
            except etree.XMLSyntaxError as e:
                self.log.error("XML parse error %s on message %s" %
                               (e, message))

    def _handleUserSession(self, data):
        data = data.UserSession
        id = str(data.Id)
        if hasattr(data.User, 'Name'):
            users = list(map(str, data.User.Name))
            if id in self.__user_session:
                new_users = list(
                    set.difference(set(users), set(self.__user_session[id])))
            else:
                # all users are new
                new_users = users

            self.__user_session[id] = users

            for user in users:
                self.preUserSession(id, user, skip_config=True)

            if len(new_users):
                self.log.debug("configuring new users: %s" % new_users)
                self.configureUsers(id, new_users)

        else:
            self.__user_session[id] = []
            self.systemSetStatus(id, "-B")

        self.log.debug("updating client '%s' user session: %s" %
                       (id, ','.join(self.__user_session[id])))

    @Command(__help__=N_("Prepare a user session after a user has logged in"),
             type="PROXY")
    def preUserSession(self, client_id, user_name, skip_config=False):
        sobj = PluginRegistry.getInstance("SchedulerService")
        # delay changes, send configuration first
        if self.env.mode == "proxy":
            # answer config locally and proceed the write-part of the call to the GOsa backend
            # TODO add MQTT call to backend MQTT-broker (with skip_config=True)
            self.log.info(
                "calling preUserSession(%s, %s, skip_config=True) on master backend"
                % (client_id, user_name))

        elif skip_config is True:
            self.__maintain_user_session(client_id, user_name)
        else:
            # normal (non-proxy) mode -> return the config and schedule the user_session maintenance
            # to prevent it from blocking the config generation
            sobj.getScheduler().add_date_job(
                self.__maintain_user_session,
                datetime.datetime.now() + datetime.timedelta(milliseconds=1),
                args=(client_id, user_name),
                tag='_internal',
                jobstore='ram')

        if skip_config is False:
            user_config = self.__collect_user_configuration(
                client_id, [user_name])
            if user_config is not None and user_name in user_config:
                return user_config[user_name]

        return None

    def __maintain_user_session(self, client_id, user_name):
        # save login time and system<->user references
        client = self.__open_device(client_id)
        client.gotoLastUser = user_name
        self.systemSetStatus(client, "+B")

        index = PluginRegistry.getInstance("ObjectIndex")
        res = index.search({"_type": "User", "uid": user_name}, {"dn": 1})
        for u in res:
            user = ObjectProxy(u["dn"])
            if not user.is_extended_by("GosaAccount"):
                user.extend("GosaAccount")
            user.gotoLastSystemLogin = datetime.datetime.now()
            user.gotoLastSystem = client.dn
            user.commit()

    @Command(__help__=N_("Cleanup a user session after a user has logged out"))
    def postUserSession(self, client_id, user):
        """
        :param client_id: clients deviceUUID
        :param user: uid of the user that has logged out
        """
        if client_id not in self.__user_session:
            self.__user_session[client_id] = []
        if user in self.__user_session[client_id]:
            self.__user_session[client_id].remove(user)
        if len(self.__user_session[client_id]) == 0:
            self.systemSetStatus(client_id, "-B")

    @Command(
        __help__="Send user configurations of all logged in user to a client")
    def configureUsers(self, client_id, users):
        users_config = self.__collect_user_configuration(client_id, users)
        for uid, config in users_config.items():
            if "menu" in config:
                # send to client
                self.log.debug("sending generated menu for user %s" % uid)
                self.queuedClientDispatch(client_id, "dbus_configureUserMenu",
                                          uid, dumps(config["menu"]))

            if "printer-setup" in config:
                self.configureHostPrinters(client_id, config["printer-setup"])

            if "resolution" in config and config[
                    "resolution"] is not None and len(config["resolution"]):
                self.log.debug(
                    "sending screen resolution: %sx%s for user %s to client %s"
                    % (config["resolution"][0], config["resolution"][1], uid,
                       client_id))
                self.queuedClientDispatch(client_id,
                                          "dbus_configureUserScreen", uid,
                                          config["resolution"][0],
                                          config["resolution"][1])

    def __collect_user_configuration(self, client_id, users):
        """
        :param client_id: deviceUUID or hostname
        :param users: list of currently logged in users on the client
        """
        if isinstance(client_id, ObjectProxy):
            client = client_id
        else:
            client = self.__open_device(client_id)
        group = None
        index = PluginRegistry.getInstance("ObjectIndex")
        res = index.search({
            "_type": "GroupOfNames",
            "member": client.dn
        }, {"dn": 1})
        if len(res) > 0:
            group = ObjectProxy(res[0]["dn"])
        config = {}

        resolution = None
        if group is not None and group.is_extended_by(
                "GotoEnvironment") and group.gotoXResolution is not None:
            resolution = group.gotoXResolution

        if client.is_extended_by(
                "GotoEnvironment") and client.gotoXResolution is not None:
            resolution = client.gotoXResolution

        release = None
        if client.is_extended_by("GotoMenu"):
            release = client.getReleaseName()
        elif group is not None and group.is_extended_by("ForemanHostGroup"):
            release = group.getReleaseName()
            parent_group = group
            while release is None and parent_group is not None and parent_group.parent_id is not None:
                res = index.search(
                    {
                        "_type": "GroupOfNames",
                        "extension": "ForemanHostGroup",
                        "foremanGroupId": parent_group.parent_id
                    }, {"dn": 1})
                if len(res) == 0:
                    break
                else:
                    parent_group = ObjectProxy(res[0]["dn"])
                    release = parent_group.getReleaseName()

        if release is None:
            self.log.error(
                "no release found for client/user combination (%s/%s)" %
                (client_id, users))

        client_menu = None

        if hasattr(client, "gotoMenu") and client.gotoMenu is not None:
            client_menu = loads(client.gotoMenu)

        # collect users DNs
        query_result = index.search({
            "_type": "User",
            "uid": {
                "in_": users
            }
        }, {"dn": 1})
        for entry in query_result:
            user = ObjectProxy(entry["dn"])
            config[user.uid] = {}

            if release is not None:
                menus = []
                if client_menu is not None:
                    menus.append(client_menu)

                # get all groups the user is member of which have a menu for the given release
                query = {
                    '_type': 'GroupOfNames',
                    "member": user.dn,
                    "extension": "GotoMenu",
                    "gotoLsbName": release
                }

                for res in index.search(query, {"gotoMenu": 1}):
                    # collect user menus
                    for m in res["gotoMenu"]:
                        menus.append(loads(m))

                if len(menus):
                    user_menu = None
                    for menu_entry in menus:
                        if user_menu is None:
                            user_menu = self.get_submenu(menu_entry)
                        else:
                            self.merge_submenu(user_menu,
                                               self.get_submenu(menu_entry))
                    config[user.uid]["menu"] = user_menu

            # collect printer settings for user, starting with the clients printers
            settings = self.__collect_printer_settings(group)
            printer_names = [x["cn"] for x in settings["printers"]]
            for res in index.search(
                {
                    '_type': 'GroupOfNames',
                    "member": user.dn,
                    "extension": "GotoEnvironment"
                }, {"dn": 1}):
                user_group = ObjectProxy(res["dn"])
                if user_group.dn == group.dn:
                    continue
                s = self.__collect_printer_settings(user_group)

                if user_group.gotoXResolution is not None:
                    resolution = user_group.gotoXResolution

                for p in s["printers"]:
                    if p["cn"] not in printer_names:
                        settings["printers"].append(p)
                        printer_names.append(p["cn"])

                if s["defaultPrinter"] is not None:
                    settings["defaultPrinter"] = s["defaultPrinter"]

            # override group environment settings if the client has one
            s = self.__collect_printer_settings(client)
            if len(s["printers"]) > 0:
                settings["printers"] = s["printers"]
                settings["defaultPrinter"] = s["defaultPrinter"]

            if user.is_extended_by(
                    "GosaAccount") and user.gosaDefaultPrinter is not None:
                # check if the users default printer is send to the client
                found = False
                for printer_settings in settings["printers"]:
                    if printer_settings["cn"] == user.gosaDefaultPrinter:
                        found = True
                        break

                def process(res):
                    if len(res) == 0:
                        self.log.warning("users defaultPrinter not found: %s" %
                                         user.gosaDefaultPrinter)
                        return None
                    elif len(res) == 1:
                        # add this one to the result set
                        printer = ObjectProxy(res[0]["dn"])
                        p_conf = {}
                        for attr in self.printer_attributes:
                            p_conf[attr] = getattr(printer, attr)
                        return p_conf
                    return False

                if found is False:
                    # find the printer and add it to the settings
                    res = index.search(
                        {
                            "_type": "GotoPrinter",
                            "cn": user.gosaDefaultPrinter
                        }, {"dn": 1})
                    printer_config = process(res)
                    if printer_config is False:
                        # more than 1 printers found by this CN, try to look in the users subtree
                        res = index.search(
                            {
                                "_type":
                                "GotoPrinter",
                                "cn":
                                user.gosaDefaultPrinter,
                                "_adjusted_parent_dn":
                                user.get_adjusted_parent_dn()
                            }, {"dn": 1})
                        printer_config = process(res)

                    if isinstance(printer_config, dict):
                        settings["printers"].append(printer_config)
                        settings["defaultPrinter"] = user.gosaDefaultPrinter
                    else:
                        self.log.warning("users defaultPrinter not found: %s" %
                                         user.gosaDefaultPrinter)
                else:
                    settings["defaultPrinter"] = user.gosaDefaultPrinter

            config[user.uid]["printer-setup"] = settings
            config[user.uid]["resolution"] = None

            if resolution is not None:
                config[user.uid]["resolution"] = [
                    int(x) for x in resolution.split("x")
                ]

            # TODO: collect and send login scripts to client
        return config

    def merge_submenu(self, menu1, menu2):
        for cn, app in menu2.get('apps', {}).items():
            if cn in menu1['apps']:
                prio1 = int(menu1[cn].get('gosaApplicationPriority', '0'))
                prio2 = int(menu2[cn].get('gosaApplicationPriority', '0'))
                if prio2 >= prio1:
                    menu1['apps'][cn] = app
            else:
                menu1['apps'][cn] = app

        for menu_entry in menu2.get('menus', {}):
            if menu_entry in menu1['menus']:
                for cn, app in menu2['menus'][menu_entry].get('apps',
                                                              {}).items():
                    if cn in menu1['menus'][menu_entry]['apps']:
                        prio1 = int(menu1['menus'][menu_entry]['apps'][cn].get(
                            'gosaApplicationPriority', '0'))
                        prio2 = int(menu2['menus'][menu_entry]['apps'][cn].get(
                            'gosaApplicationPriority', '0'))
                        if prio2 >= prio1:
                            menu1['menus'][menu_entry]['apps'][cn] = app
                    else:
                        menu1['menus'][menu_entry]['apps'][cn] = app
            else:
                menu1['menus'][menu_entry] = menu2['menus'][menu_entry]

            if 'menus' in menu2['menus'][menu_entry]:
                if menu_entry in menu1['menus'] and 'menus' in menu1['menus'][
                        menu_entry]:
                    self.merge_submenu(menu1['menus'][menu_entry],
                                       menu2['menus'][menu_entry])
                else:
                    menu1['menus'][menu_entry]['menus'] = menu2['menus'][
                        menu_entry]['menus']

    def get_submenu(self, entries):
        result = None
        for entry in entries:
            if result is None:
                result = {'apps': {}}

            if 'children' in entry:
                if not 'menus' in result:
                    result['menus'] = {}
                result['menus'][entry.get('name',
                                          N_('Unbekannt'))] = self.get_submenu(
                                              entry['children'])
            else:
                application = self.get_application(entry)
                result['apps'][application.get('cn', 'name')] = application

        return result

    def get_application(self, application):
        result = None
        if 'name' in application and 'dn' in application:
            result = {'name': application.get('name')}
            if 'gosaApplicationParameter' in application:
                result['gosaApplicationParameter'] = application.get(
                    'gosaApplicationParameter')

            application = ObjectProxy(application.get('dn'))
            if application is not None:
                for attribute in self.entry_attributes:
                    if hasattr(application, attribute):
                        attribute_name = self.entry_map.get(
                            attribute, attribute)
                        result[attribute_name] = getattr(
                            application, attribute)

        return result

    @Command(__help__=N_(
        "Send user specific configuration (e.g. printers) to a clients active user sessions"
    ))
    def configureClient(self, client_id):
        """
        :param client_id: deviceUUID or hostname
        """
        if client_id in self.__user_session:
            self.configureUsers(client_id, self.__user_session[client_id])

    def configureHostPrinters(self, client_id, config):
        """ configure the printers for this client via dbus. """
        if "printers" not in config or len(config["printers"]) == 0:
            return
        # delete old printers first
        self.queuedClientDispatch(client_id, "dbus_deleteAllPrinters")

        for p_conf in config["printers"]:
            self.queuedClientDispatch(client_id, "dbus_addPrinter", p_conf)

        if "defaultPrinter" in config and config["defaultPrinter"] is not None:
            self.queuedClientDispatch(client_id, "dbus_defaultPrinter",
                                      config["defaultPrinter"])

    def __collect_printer_settings(self, object):
        settings = {"printers": [], "defaultPrinter": None}
        if object is not None and object.is_extended_by(
                "GotoEnvironment") and len(object.gotoPrinters):
            # get default printer
            settings["defaultPrinter"] = object.gotoDefaultPrinter

            # collect printer PPDs
            for printer_dn in object.gotoPrinters:
                printer = ObjectProxy(printer_dn)
                p_conf = {}
                for attr in self.printer_attributes:
                    p_conf[attr] = getattr(printer, attr) if getattr(
                        printer, attr) is not None else ""
                    if attr == "gotoPrinterPPD" and p_conf[attr] != "":
                        p_conf[attr] = self.ppd_proxy.getPPDURL(p_conf[attr])

                settings["printers"].append(p_conf)
        return settings

    def _handleClientPing(self, data):
        data = data.ClientPing
        client = data.Id.text
        self.__set_client_online(data.Id.text)
        if client in self.__client:
            self.__client[client]['last-seen'] = datetime.datetime.utcnow()

    def _handleClientSignature(self, data):
        data = data.ClientSignature
        client = data.Id.text
        self.log.info("client '%s' has an signature update for us" % client)

        # Remove remaining proxy values for this client
        if client in self.__proxy:
            self.__proxy[client].close()
            del self.__proxy[client]

        # Assemble caps
        caps = {}
        for method in data.ClientCapabilities.ClientMethod:
            self.log.debug("client %s provides method %s" %
                           (client, method.Name.text))
            caps[method.Name.text] = {
                'path': method.Path.text,
                'sig': method.Signature.text,
                'doc': method.Documentation.text
            }

        # This may happen if we get a stuck event
        if not data.Id.text in self.__client:
            return

        # Decide if we need to notify someone about new methods
        current = copy(self.__client[data.Id.text]['caps'])
        self.__client[data.Id.text]['caps'] = caps
        for method in [m for m in current.keys() if not m in caps]:
            self.notify_listeners(data.Id.text, method, False)
        for method in [m for m in caps if not m in current.keys()]:
            self.notify_listeners(data.Id.text, method, True)

    def notify_listeners(self, cid, method, status):
        if method in self.__listeners:
            for cb in self.__listeners[method]:
                cb(cid, method, status)

    def register_listener(self, method, callback):
        if not method in self.__listeners:
            self.__listeners[method] = []

        if not callback in self.__listeners[method]:
            self.__listeners[method].append(callback)

    def unregister_listener(self, method, callback):
        if not method in self.__listeners:
            return

        if not callback in self.__listeners[method]:
            return

        self.__listeners[method].pop(self.__listeners[method].index(callback))

    def _handleClientAnnounce(self, data):
        data = data.ClientAnnounce
        client = data.Id.text
        self.log.info("client '%s' is joining us" % client)
        self.systemSetStatus(client, "+O-o")

        # Assemble network information
        network = {}
        for interface in data.NetworkInformation.NetworkDevice:
            network[interface.Name.text] = {
                'IPAddress': interface.IPAddress.text,
                'IPv6Address': interface.IPv6Address.text,
                'MAC': interface.MAC.text,
                'Netmask': interface.Netmask.text,
                'Broadcast': interface.Broadcast.text
            }

        # Add recieve time to be able to sort out dead nodes
        t = datetime.datetime.utcnow()
        info = {
            'name': data.Name.text,
            'last-seen': t,
            'online': True,
            'caps': {},
            'network': network
        }

        self.__client[client] = info

    def _on_client_caps(self, cid, method, status):
        if status is False:
            return

        self.log.debug("client %s provides method %s" % (cid, method))
        if cid in self.__client_call_queue and method in self.__client_call_queue[
                cid]:
            for arg, larg in self.__client_call_queue[cid][method]:
                self.clientDispatch(cid, method, *arg, **larg)
            del self.__client_call_queue[cid][method]

            if len(self.__client_call_queue[cid].keys()) == 0:
                del self.__client_call_queue[cid]

            # check if we still have listeners for that method
            delete = True
            for client_id, queue in self.__client_call_queue.items():
                if method in queue:
                    delete = False
                    break

            if delete is True:
                self.unregister_listener(method, self._on_client_caps)

    def _handleClientLeave(self, data):
        data = data.ClientLeave
        client = data.Id.text
        self.log.info("client '%s' is leaving" % client)
        self.__set_client_offline(client, True)

    def __set_client_online(self, client):
        self.systemSetStatus(client, "+O-o")
        if client in self.__client:
            self.__client[client]['online'] = True

    def __set_client_offline(self, client, purge=False):
        try:
            self.systemSetStatus(client, "-O+o")
        except ValueError as e:
            id = C.get_error_id(str(e))
            error = C.getError(None, None, id, keep=True)
            if error.status_code == 404:
                pass
            else:
                raise e

        if client in self.__client:
            if purge:
                del self.__client[client]

                if client in self.__proxy:
                    self.__proxy[client].close()
                    del self.__proxy[client]

                if client in self.__user_session:
                    del self.__user_session[client]

            else:
                self.__client[client]['online'] = False

    def __gc(self):
        interval = int(self.env.config.get("goto.timeout", default="600"))

        for client, info in self.__client.items():
            if not info['online']:
                continue

            if info['last-seen'] < datetime.datetime.utcnow(
            ) - datetime.timedelta(seconds=2 * interval):
                self.log.info("client '%s' looks dead - setting to 'offline'" %
                              client)
                self.__set_client_offline(client)

    def applyClientRights(self, device_uuid):
        # check rights
        acl = PluginRegistry.getInstance("ACLResolver")
        if not acl.check(
                device_uuid, "%s.%s.%s" %
            (self.env.domain, "command", "preUserSession"), "x"):
            role_name = "$$ClientDevices"
            # create AclRole for joining if not exists
            index = PluginRegistry.getInstance("ObjectIndex")
            res = index.search({
                "_type": "AclRole",
                "name": role_name
            }, {"dn": 1})
            if len(res) == 0:
                # create
                role = ObjectProxy(self.env.base, "AclRole")
                role.name = role_name

                # create rule
                aclentry = {
                    "priority":
                    0,
                    "scope":
                    "sub",
                    "actions": [{
                        "topic":
                        "%s\.command\.(joinClient|preUserSession|postUserSession|getMethods)"
                        % self.env.domain,
                        "acl":
                        "x",
                        "options": {}
                    }]
                }
                role.AclRoles = [aclentry]
                role.commit()

            # check if device has role
            found = False
            base = ObjectProxy(self.env.base)
            if base.is_extended_by("Acl"):
                for entry in base.AclSets:
                    if entry["rolename"] == role_name and device_uuid in entry[
                            "members"]:
                        found = True
                        break
            else:
                base.extend("Acl")

            if found is False:
                acl_entry = {
                    "priority": 0,
                    "members": [device_uuid],
                    "rolename": role_name
                }
                base.AclSets.append(acl_entry)
                base.commit()