Skip to content
ccserver.py 9.57 KiB
Newer Older
Antoine Millet's avatar
Antoine Millet committed
#!/usr/bin/env python
#coding=utf8

Antoine Millet's avatar
Antoine Millet committed
'''
Main class of cc-server.
'''

from __future__ import absolute_import

Antoine Millet's avatar
Antoine Millet committed
import logging
import socket
from fnmatch import fnmatch as glob
Antoine Millet's avatar
Antoine Millet committed
from sjrpc.server import SimpleSslRpcServer

from ccserver.handlers import WelcomeHandler
from ccserver.conf import CCConf
from ccserver.client import CCClient
from ccserver.exceptions import AlreadyRegistered, NotConnectedAccountError
from ccserver.orderedset import OrderedSet
from ccserver.tql import TqlParser, TqlObject
from ccserver.objectsdb import ObjectsDB
Antoine Millet's avatar
Antoine Millet committed
from ccserver.jobs import JobsManager
Antoine Millet's avatar
Antoine Millet committed
class CCServer(object):
    '''
    CloudControl server main class.

    :param conf_dir: the directory that store the client configuration
    :param certfile: the path to the ssl certificate
    :param keyfile: the path to the ssl key
    :param address: the interface to bind
    :param port: the port to bind
    '''

    LISTEN_BACKLOG = 5

    # These tags are reserved and cannot be setted by an user:
    RESERVED_TAGS = ('id', 'a', 'r', 'close', 'con', 'ip', 'p')
    def __init__(self, conf_dir, maxcon, maxidle, certfile=None, keyfile=None,
Antoine Millet's avatar
Antoine Millet committed
                 address='0.0.0.0', port=1984):

        # Dict containing all connected accounts, the key is the login of
        # the account and the value the :class:`RpcConnection` of the peer:
        self._connected = {}

        # The interface object to the configuration directory:
        self.conf = CCConf(conf_dir)

        # Some settings:
        self._maxcon = maxcon
        self._maxidle = maxidle

Antoine Millet's avatar
Antoine Millet committed
        # Create the server socket
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.bind((address, port))
        sock.listen(CCServer.LISTEN_BACKLOG)

        logging.info('Server started to running')

Antoine Millet's avatar
Antoine Millet committed
        if certfile:
            logging.info('SSL Certificate: %s', certfile)
Antoine Millet's avatar
Antoine Millet committed
        if keyfile:
            logging.info('SSL Key: %s', certfile)
Antoine Millet's avatar
Antoine Millet committed

        logging.info('Listening on %s:%s', address, port)
Antoine Millet's avatar
Antoine Millet committed
        # Create the connection manager:
        self.manager = SimpleSslRpcServer(sock, certfile=certfile,
                                          keyfile=keyfile,
                                          default_handler=WelcomeHandler(self),
                                          on_disconnect='on_disconnect')
Antoine Millet's avatar
Antoine Millet committed

Antoine Millet's avatar
Antoine Millet committed
        # The jobs manager:
        self.jobs = JobsManager(self)

    def _update_accounts(self):
        '''
        Update the database with accounts.
        '''

        all_objects = self.objects.all((), ())
        db_accounts = set([o['a'] for o in all_objects if 'a' in o])
        accounts = set(self.conf.list_accounts())

        to_register = accounts - db_accounts
        to_unregister = db_accounts - accounts

        for login in to_register:
            conf = self.conf.show(login)
            obj = TqlObject(id=login, r=conf['role'], a=login)
            self.objects.register(obj, cleanable=('ip', 'con'))

        for login in to_unregister:
            self.objects.unregister(login)

    def iter_connected_role(self, role=None):
Antoine Millet's avatar
Antoine Millet committed
        '''
        Generator to iter on each connected client with specified role. If role
        is None, return all connected clients.
Antoine Millet's avatar
Antoine Millet committed

        :param role: role to filter
        '''

        for login, client in self._connected.items():
            if role is None or client.role == role:
Antoine Millet's avatar
Antoine Millet committed
                yield client

    def register(self, login, role, connection):
        '''
        Register a new connected account on the server.

        :param login: login of the account
        :param connection: connection to register
        :param tags: tags to add for the client
        '''

        if login in self._connected:
            raise AlreadyRegistered('A client is already connected with this '
                                    'account.')
        else:
            self._connected[login] = CCClient(login, role, self, connection)
Antoine Millet's avatar
Antoine Millet committed

    def unregister(self, connection):
        '''
        Unregister an already connected account.

        :param connection: the connection of the client to unregister
        '''
Antoine Millet's avatar
Antoine Millet committed

        client = self.search_client_by_connection(connection)

        # Unregister objects from database if it have no account attached:
        obj = self.objects.get(client.login)
        if obj is not None and 'a' not in obj:
            self.objects.unregister(obj['id'])

        if client.login in self._connected:
            del self._connected[client.login]
        self.objects.unregister_children(client.login)
        self.objects.clean_ttls(client.login)

    def sub_register(self, parent, name, role):
        '''
        Register a new node supervised by a parent.

        :param parent: the parent login of the subnode
        :param login: the name of the subnode
        :param role: the role of the subnode
        '''

        obj_parent = self.objects.get_by_id(parent)
        oid = '%s.%s' % (parent, name)
        obj = TqlObject(id=oid, r=role, __parent=obj_parent, p=obj_parent['id'])
        self.objects.register(obj)

    def sub_unregister(self, parent, name):
        '''
        Unregister a node supervised by a parent.

        :param parent: the parent of the subnode
        :param login: the name of the subnode
        '''

        oid = '%s.%s' % (parent, name)
        self.objects.unregister(oid)
    def search_client_by_connection(self, connection):
        '''
        Search a connected client by it connection. If no client is found,
        return None.

        :param connection: the connection of the client to search
        :return: the found client or None
        '''

        for client in self._connected.values():
            if client.connection is connection:
                return client
        else:
            return None

Antoine Millet's avatar
Antoine Millet committed
    def run(self):
        '''
        Run the server mainloop.
        '''

        # Register accounts on the database:
        self._update_accounts()

        # Running server internal jobs:
        self.jobs.create('kill_oldcli', maxcon=self._maxcon,
                         maxidle=self._maxidle, _hidden=True)

        logging.debug('Running manager mainloop')
Antoine Millet's avatar
Antoine Millet committed
        self.manager.run()

    def get_connection(self, login):
        '''
        Get the connection of a connecter account login.

        :param login: login of the connection to get
        :return: :class:`RpcConnection` instance of the peer connection
        '''

        return self._connected[login]
    def kill(self, login):
        '''
        Disconnect from the server the client identified by provided login.

        :param login: the login of the user to disconnect
        :throws NotConnectedAccount: when provided account is not connected (or
            if account doesn't exists).
        '''

        client = self._connected.get(login)
        if client is None:
            raise NotConnectedAccountError('The account %s is not '
                                           'connected' % login)
        client.shutdown()

    def check(self, login, method, tql=None):
Antoine Millet's avatar
Antoine Millet committed
        '''
        Check if the user can call the specified method with specified TQL.

        :param login: the login of the user
        :param method: the method to call
        :param tql: the tql passed in argument of the method
        :return: True if user have rights, else False
        '''

        rights = self.conf.show(login)['rights']
        if tql is not None:
            objects = self.list(tql, pure=True)
Antoine Millet's avatar
Antoine Millet committed
        for right in rights:
            if not (right['method'] is None or glob(method, right['method'])):
Antoine Millet's avatar
Antoine Millet committed
                continue
            if tql is not None and right['tql']:
Antoine Millet's avatar
Antoine Millet committed
                objects_right = self.list(right['tql'], pure=True)
                if set(objects) <= set(objects_right):
                    return right['target'] == 'allow'
            else:
                return right['target'] == 'allow'
        return False
    def list(self, query, show=set(), pure=False, return_toshow=False):
        :param query: the TQL to use to selection objects on list.
        self._update_accounts()

        parser = TqlParser(query)
        ast, to_show, to_get, to_check = parser.parse()
        orig_to_show = copy(to_show)
        to_show += show

        # Calculate the tags to get/check/show:
        if to_get is not None:
            to_get -= set((self.RESERVED_TAGS))
        if to_check is not None:
            to_check -= set((self.RESERVED_TAGS))
        
        deny = set()

        for tag in copy(to_show):
            if tag == '*':
                to_show = None
                deny.clear()
            elif tag.startswith('-'):
                tag = tag[1:]
                deny.add(tag)
                if to_show is not None and tag in to_show:
        if to_show is None:
            to_display = None
        else:
            to_display = set(to_show) | to_get

        if to_show is not None:
            to_show = set(to_show)
            to_show -= set((self.RESERVED_TAGS))

        objects = OrderedSet(self.objects.all(to_get, to_check))
        if ast is not None:
            objects, _ = ast.eval(objects, objects)
        ids = [x['id'] for x in objects]
Antoine Millet's avatar
Antoine Millet committed

        objects = self.objects.some(ids, to_show)
            objects_dicts.append(obj.to_dict(to_display, deny=deny))
        if return_toshow:
            return objects_dicts, orig_to_show
        else:
            return objects_dicts