Beispiel #1
0
 def __init__(self, settings):
     self.settings = settings
     self.SIGNATURE = "LocalfsFileClient"
     self.ROOTPATH = settings.local_root_path
     self.CACHEPATH = settings.cache_path
     self.syncer_dbtuple = settings.syncer_dbtuple
     client_dbname = self.SIGNATURE + '.db'
     self.client_dbtuple = common.DBTuple(dbtype=database.ClientDB,
                                          dbname=utils.join_path(
                                              settings.instance_path,
                                              client_dbname))
     database.initialize(self.client_dbtuple)
     self.probe_candidates = utils.ThreadSafeDict()
     self.check_enabled()
Beispiel #2
0
 def __init__(self, settings):
     self.settings = settings
     self.SIGNATURE = "PithosFileClient"
     self.auth_url = settings.auth_url
     self.auth_token = settings.auth_token
     self.container = settings.container
     self.syncer_dbtuple = settings.syncer_dbtuple
     client_dbname = self.SIGNATURE + '.db'
     self.client_dbtuple = common.DBTuple(dbtype=database.ClientDB,
                                          dbname=utils.join_path(
                                              settings.instance_path,
                                              client_dbname))
     database.initialize(self.client_dbtuple)
     self.endpoint = settings.endpoint
     self.last_modification = "0000-00-00"
     self.probe_candidates = utils.ThreadSafeDict()
     self.check_enabled()
Beispiel #3
0
 def __init__(self, settings, master, slave):
     self.settings = settings
     self.master = master
     self.slave = slave
     self.DECISION = 'DECISION'
     self.SYNC = 'SYNC'
     self.MASTER = master.SIGNATURE
     self.SLAVE = slave.SIGNATURE
     self.syncer_dbtuple = settings.syncer_dbtuple
     self.clients = {self.MASTER: master, self.SLAVE: slave}
     self.notifiers = {}
     self.decide_thread = None
     self.sync_threads = []
     self.failed_serials = utils.ThreadSafeDict()
     self.sync_queue = Queue.Queue()
     self.messager = settings.messager
     self.heartbeat = self.settings.heartbeat
Beispiel #4
0
class WebSocketProtocol(WebSocket):
    """Helper-side WebSocket protocol for communication with GUI:

    -- INTERNAL HANDSAKE --
    GUI: {"method": "post", "ui_id": <GUI ID>}
    HELPER: {"ACCEPTED": 202, "action": "post ui_id"}" or
        "{"REJECTED": 401, "action": "post ui_id"}

    -- ERRORS WITH SIGNIFICANCE --
    -- SHUT DOWN --
    GUI: {"method": "post", "path": "shutdown"}

    -- PAUSE --
    GUI: {"method": "post", "path": "pause"}
    HELPER: {"OK": 200, "action": "post pause"} or error

    -- START --
    GUI: {"method": "post", "path": "start"}
    HELPER: {"OK": 200, "action": "post start"} or error

    -- FORCE START --
    GUI: {"method": "post", "path": "force"}
    HELPER: {"OK": 200, "action": "post force"} or error

    -- GET SETTINGS --
    GUI: {"method": "get", "path": "settings"}
    HELPER:
        {
            "action": "get settings",
            "token": <user token>,
            "url": <auth url>,
            "container": <container>,
            "directory": <local directory>,
            "exclude": <file path>,
            "language": <en|el>,
            "ask_to_sync": <true|false>
        } or {<ERROR>: <ERROR CODE>}

    -- PUT SETTINGS --
    GUI: {
            "method": "put", "path": "settings",
            "token": <user token>,
            "url": <auth url>,
            "container": <container>,
            "directory": <local directory>,
            "exclude": <file path>,
            "language": <en|el>,
            "ask_to_sync": <true|false>
        }
    HELPER: {"CREATED": 201, "action": "put settings",} or
        {<ERROR>: <ERROR CODE>, "action": "get settings",}

    -- GET STATUS --
    GUI: {"method": "get", "path": "status"}
    HELPER: {"code": <int>,
            "synced": <int>, "unsynced": <int>, "failed": <int>,
            "action": "get status"
        } or {<ERROR>: <ERROR CODE>, "action": "get status"}
    """
    status = utils.ThreadSafeDict()
    with status.lock() as d:
        d.update(code=STATUS['UNINITIALIZED'], synced=0, unsynced=0, failed=0)

    ui_id = None
    session_db = None
    accepted = False
    settings = dict(token=None,
                    url=None,
                    container=None,
                    directory=None,
                    exclude=None,
                    ask_to_sync=True,
                    language="en")
    cnf = AgkyraConfig()
    essentials = ('url', 'token', 'container', 'directory')

    def get_status(self, key=None):
        """:return: updated status dict or value of specified key"""
        if self.syncer and self.can_sync():
            self._consume_messages()
        with self.status.lock() as d:
            LOGGER.debug('Status is now %s' % d['code'])
            return d.get(key, None) if key else dict(d)

    def set_status(self, **kwargs):
        with self.status.lock() as d:
            LOGGER.debug('Set status to %s' % kwargs)
            d.update(kwargs)

    @property
    def syncer(self):
        """:returns: the first syncer object or None"""
        with SYNCERS.lock() as d:
            for sync_key, sync_obj in d.items():
                return sync_obj
        return None

    def clean_db(self):
        """Clean DB from current session trace"""
        LOGGER.debug('Remove current session trace')
        with database.TransactedConnection(self.session_db) as db:
            db.unregister_heartbeat(self.ui_id)

    def shutdown_syncer(self, syncer_key=0, timeout=None):
        """Shutdown the syncer backend object"""
        LOGGER.debug('Shutdown syncer')
        with SYNCERS.lock() as d:
            syncer = d.pop(syncer_key, None)
            if syncer and self.can_sync():
                remaining = syncer.stop_all_daemons(timeout=timeout)
                LOGGER.debug('Wait open syncs to complete')
                syncer.wait_sync_threads(timeout=remaining)

    def _get_default_sync(self):
        """Get global.default_sync or pick the first sync as default
        If there are no syncs, create a 'default' sync.
        """
        sync = self.cnf.get('global', 'default_sync')
        if not sync:
            for sync in self.cnf.keys('sync'):
                break
            self.cnf.set('global', 'default_sync', sync or 'default')
        return sync or 'default'

    def _get_sync_cloud(self, sync):
        """Get the <sync>.cloud or pick the first cloud and use it
        In case of cloud picking, set the cloud as the <sync>.cloud for future
        sessions.
        If no clouds are found, create a 'default' cloud, with an empty url.
        """
        try:
            cloud = self.cnf.get_sync(sync, 'cloud')
        except KeyError:
            cloud = None
        if not cloud:
            for cloud in self.cnf.keys('cloud'):
                break
            self.cnf.set_sync(sync, 'cloud', cloud or 'default')
        return cloud or 'default'

    def _load_settings(self):
        LOGGER.debug('Start loading settings')
        sync = self._get_default_sync()
        cloud = self._get_sync_cloud(sync)

        for option in ('url', 'token'):
            try:
                value = self.cnf.get_cloud(cloud, option)
                if not value:
                    raise Exception()
                self.settings[option] = value
            except Exception:
                self.settings[option] = None
                self.set_status(code=STATUS['SETTINGS MISSING'])

        self.settings['ask_to_sync'] = (self.cnf.get('global',
                                                     'ask_to_sync') == 'on')
        self.settings['language'] = self.cnf.get('global', 'language')

        # for option in ('container', 'directory', 'exclude'):
        for option in ('container', 'directory'):
            try:
                value = self.cnf.get_sync(sync, option)
                if not value:
                    raise KeyError()
                self.settings[option] = value
            except KeyError:
                LOGGER.debug('No %s is set' % option)
                self.set_status(code=STATUS['SETTINGS MISSING'])

        LOGGER.debug('Finished loading settings')

    def _dump_settings(self):
        LOGGER.debug('Saving settings')
        sync = self._get_default_sync()

        cloud = self._get_sync_cloud(sync)
        new_url = self.settings.get('url') or ''
        new_token = self.settings.get('token') or ''

        try:
            old_url = self.cnf.get_cloud(cloud, 'url') or ''
        except KeyError:
            old_url = new_url

        while old_url and old_url != new_url:
            cloud = '%s_%s' % (cloud, sync)
            try:
                self.cnf.get_cloud(cloud, 'url')
            except KeyError:
                break

        LOGGER.debug('Cloud name is %s' % cloud)
        self.cnf.set_cloud(cloud, 'url', new_url)
        self.cnf.set_cloud(cloud, 'token', new_token)
        self.cnf.set_sync(sync, 'cloud', cloud)

        LOGGER.debug('Save sync settings, name is %s' % sync)
        # for option in ('directory', 'container', 'exclude'):
        for option in ('directory', 'container'):
            self.cnf.set_sync(sync, option, self.settings.get(option) or '')

        self.cnf.set('global', 'language', self.settings.get('language', 'en'))
        ask_to_sync = self.settings.get('ask_to_sync', True)
        self.cnf.set('global', 'ask_to_sync', 'on' if ask_to_sync else 'off')

        self.cnf.write()
        LOGGER.debug('Settings saved')

    def _essentials_changed(self, new_settings):
        """Check if essential settings have changed in new_settings"""
        return any(
            [self.settings[e] != new_settings[e] for e in self.essentials])

    def _consume_messages(self, max_consumption=10):
        """Update status by consuming and understanding syncer messages"""
        if self.can_sync():
            msg = self.syncer.get_next_message()
            # if not msg:
            #     with self.status.lock() as d:
            #         if d['unsynced'] == d['synced'] + d['failed']:
            #             d.update(unsynced=0, synced=0, failed=0)
            while msg:
                if isinstance(msg, messaging.SyncMessage):
                    LOGGER.debug('UNSYNCED +1 %s' %
                                 getattr(msg, 'objname', ''))
                    self.set_status(unsynced=self.get_status('unsynced') + 1)
                elif isinstance(msg, messaging.AckSyncMessage):
                    LOGGER.debug('SYNCED +1 %s' % getattr(msg, 'objname', ''))
                    self.set_status(synced=self.get_status('synced') + 1)
                elif isinstance(msg, messaging.SyncErrorMessage):
                    LOGGER.debug('FAILED +1 %s' % getattr(msg, 'objname', ''))
                    self.set_status(failed=self.get_status('failed') + 1)
                elif isinstance(msg, messaging.LocalfsSyncDisabled):
                    LOGGER.debug('STOP BACKEND, %s' %
                                 getattr(msg, 'objname', ''))
                    LOGGER.debug('CHANGE STATUS TO: %s' %
                                 STATUS['DIRECTORY ERROR'])
                    self.set_status(code=STATUS['DIRECTORY ERROR'])
                    self.syncer.stop_all_daemons()
                elif isinstance(msg, messaging.PithosSyncDisabled):
                    LOGGER.debug('STOP BACKEND, %s' %
                                 getattr(msg, 'objname', ''))
                    self.set_status(code=STATUS['CONTAINER ERROR'])
                    self.syncer.stop_all_daemons()
                elif isinstance(msg, messaging.PithosAuthTokenError):
                    LOGGER.debug('STOP BACKEND, %s' %
                                 getattr(msg, 'objname', ''))
                    self.set_status(code=STATUS['TOKEN ERROR'])
                    self.syncer.stop_all_daemons()
                elif isinstance(msg, messaging.PithosGenericError):
                    LOGGER.debug('STOP BACKEND, %s' %
                                 getattr(msg, 'objname', ''))
                    self.set_status(code=STATUS['CRITICAL ERROR'])
                    self.syncer.stop_all_daemons()
                LOGGER.debug('Backend message: %s %s' % (msg.name, type(msg)))
                # Limit the amount of messages consumed each time
                max_consumption -= 1
                if max_consumption:
                    msg = self.syncer.get_next_message()
                else:
                    break

    def can_sync(self):
        """Check if settings are enough to setup a syncing proccess"""
        return all([self.settings[e] for e in self.essentials])

    def init_sync(self, leave_paused=False):
        """Initialize syncer"""
        self.set_status(code=STATUS['INITIALIZING'])
        sync = self._get_default_sync()

        kwargs = dict(agkyra_path=AGKYRA_DIR)
        # Get SSL settings
        cloud = self._get_sync_cloud(sync)
        try:
            ignore_ssl = self.cnf.get_cloud(cloud, 'ignore_ssl') in ('on', )
            kwargs['ignore_ssl'] = ignore_ssl
        except KeyError:
            ignore_ssl = None
        if not ignore_ssl:
            try:
                kwargs['ca_certs'] = self.cnf.get_cloud(cloud, 'ca_certs')
            except KeyError:
                pass

        syncer_ = None
        try:
            syncer_settings = setup.SyncerSettings(self.settings['url'],
                                                   self.settings['token'],
                                                   self.settings['container'],
                                                   self.settings['directory'],
                                                   **kwargs)
            master = pithos_client.PithosFileClient(syncer_settings)
            slave = localfs_client.LocalfsFileClient(syncer_settings)
            syncer_ = syncer.FileSyncer(syncer_settings, master, slave)
            # Check if syncer is ready, by consuming messages
            local_ok, remote_ok = False, False
            for i in range(2):

                LOGGER.debug('Get message %s' % (i + 1))
                msg = syncer_.get_next_message(block=True)
                LOGGER.debug('Got message: %s' % msg)

                if isinstance(msg, messaging.LocalfsSyncDisabled):
                    self.set_status(code=STATUS['DIRECTORY ERROR'])
                    local_ok = False
                    break
                elif isinstance(msg, messaging.PithosSyncDisabled):
                    self.set_status(code=STATUS['CONTAINER ERROR'])
                    remote_ok = False
                    break
                elif isinstance(msg, messaging.LocalfsSyncEnabled):
                    local_ok = True
                elif isinstance(msg, messaging.PithosSyncEnabled):
                    remote_ok = True
                else:
                    LOGGER.error('Unexpected message %s' % msg)
                    self.set_status(code=STATUS['CRITICAL ERROR'])
                    break
            if local_ok and remote_ok:
                syncer_.initiate_probe()
                new_status = 'PAUSED' if leave_paused else 'READY'
                self.set_status(code=STATUS[new_status])
        except pithos_client.ClientError as ce:
            LOGGER.debug('backend init failed: %s %s' % (ce, ce.status))
            try:
                code = {
                    400: STATUS['AUTH URL ERROR'],
                    401: STATUS['TOKEN ERROR'],
                }[ce.status]
            except KeyError:
                code = STATUS['UNINITIALIZED']
            self.set_status(code=code)
        finally:
            self.set_status(synced=0, unsynced=0)
            with SYNCERS.lock() as d:
                d[0] = syncer_

    # Syncer-related methods
    def get_settings(self):
        return self.settings

    def set_settings(self, new_settings):
        """Set the settings and dump them to permanent storage if needed"""
        # Prepare setting save
        old_status = self.get_status('code')
        ok_not_syncing = [STATUS['READY'], STATUS['PAUSING'], STATUS['PAUSED']]
        active = ok_not_syncing + [STATUS['SYNCING']]

        must_reset_syncing = self._essentials_changed(new_settings)
        if must_reset_syncing and old_status in active:
            LOGGER.debug('Temporary backend shutdown to save settings')
            self.shutdown_syncer()

        # save settings
        self.settings.update(new_settings)
        self._dump_settings()

        # Restart
        LOGGER.debug('Reload settings')
        self._load_settings()
        can_sync = must_reset_syncing and self.can_sync()
        if can_sync:
            leave_paused = old_status in ok_not_syncing
            LOGGER.debug('Restart backend')
            self.init_sync(leave_paused=leave_paused)

    def _pause_syncer(self):
        syncer_ = self.syncer
        syncer_.stop_decide()
        LOGGER.debug('Wait open syncs to complete')
        syncer_.wait_sync_threads()

    def pause_sync(self):
        """Pause syncing (assuming it is up and running)"""
        if self.syncer:
            self.set_status(code=STATUS['PAUSING'])
            self.syncer.stop_decide()
            self.set_status(code=STATUS['PAUSED'])

    def start_sync(self):
        """Start syncing"""
        self.syncer.start_decide()
        self.set_status(code=STATUS['SYNCING'])

    def force_sync(self):
        """Force syncing, assuming there is a directory or container problem"""
        self.set_status(code=STATUS['INITIALIZING'])
        self.syncer.settings.purge_db_archives_and_enable()
        self.syncer.initiate_probe()
        self.set_status(code=STATUS['READY'])

    def send_json(self, msg):
        LOGGER.debug('send: %s' % msg)
        self.send(json.dumps(msg))

    # Protocol handling methods
    def _post(self, r):
        """Handle POST requests"""
        if self.accepted:
            action = r['path']
            if action == 'shutdown':
                # Clean db to cause syncer backend to shut down
                self.set_status(code=STATUS['SHUTTING DOWN'])
                self.shutdown_syncer(timeout=5)
                self.clean_db()
                return
            {
                'init': self.init_sync,
                'start': self.start_sync,
                'pause': self.pause_sync,
                'force': self.force_sync
            }[action]()
            self.send_json({'OK': 200, 'action': 'post %s' % action})
        elif r['ui_id'] == self.ui_id:
            self.accepted = True
            self.send_json({'ACCEPTED': 202, 'action': 'post ui_id'})
            self._load_settings()
            status = self.get_status('code')
            if self.can_sync() and status == STATUS['UNINITIALIZED']:
                self.set_status(code=STATUS['SETTINGS READY'])
        else:
            action = r.get('path', 'ui_id')
            self.send_json({'REJECTED': 401, 'action': 'post %s' % action})
            self.terminate()

    def _put(self, r):
        """Handle PUT requests"""
        if self.accepted:
            LOGGER.debug('put %s' % r)
            action = r.pop('path')
            self.set_settings(r)
            r.update({'CREATED': 201, 'action': 'put %s' % action})
            self.send_json(r)
        else:
            action = r['path']
            self.send_json({
                'UNAUTHORIZED UI': 401,
                'action': 'put %s' % action
            })
            self.terminate()

    def _get(self, r):
        """Handle GET requests"""
        action = r.pop('path')
        if not self.accepted:
            self.send_json({
                'UNAUTHORIZED UI': 401,
                'action': 'get %s' % action
            })
            self.terminate()
        else:
            data = {
                'settings': self.get_settings,
                'status': self.get_status,
            }[action]()
            data['action'] = 'get %s' % action
            self.send_json(data)

    def received_message(self, message):
        """Route requests to corresponding handling methods"""
        try:
            r = json.loads('%s' % message)
        except ValueError as ve:
            self.send_json({'BAD REQUEST': 400})
            LOGGER.error('JSON ERROR: %s' % ve)
            return
        try:
            method = r.pop('method')
            {'post': self._post, 'put': self._put, 'get': self._get}[method](r)
        except KeyError as ke:
            action = method + ' ' + r.get('path', '')
            self.send_json({'BAD REQUEST': 400, 'action': action})
            LOGGER.error('KEY ERROR: %s' % ke)
        except setup.ClientError as ce:
            action = '%s %s' % (method,
                                r.get('path', 'ui_id' if 'ui_id' in r else ''))
            self.send_json({'%s' % ce: ce.status, 'action': action})
            return
        except Exception as e:
            self.send_json({'INTERNAL ERROR': 500})
            reason = '%s %s' % (method or '', r)
            LOGGER.error('EXCEPTION (%s): %s' % (reason, e))
            self.terminate()
Beispiel #5
0
                           messaging, utils, database, common)
from agkyra.config import AgkyraConfig, AGKYRA_DIR

if getattr(sys, 'frozen', False):
    # we are running in a |PyInstaller| bundle
    BASEDIR = sys._MEIPASS
    ISFROZEN = True
else:
    # we are running in a normal Python environment
    BASEDIR = os.path.dirname(os.path.realpath(__file__))
    ISFROZEN = False

RESOURCES = os.path.join(BASEDIR, 'resources')

LOGGER = logging.getLogger(__name__)
SYNCERS = utils.ThreadSafeDict()

with open(os.path.join(RESOURCES, 'ui_data/common_en.json')) as f:
    COMMON = json.load(f)

with open(os.path.join(RESOURCES, 'main.json')) as f:
    MAINSETTINGS = json.load(f)

STATUS = MAINSETTINGS['STATUS']


class SessionDB(database.DB):
    def init(self):
        db = self.db
        db.execute('CREATE TABLE IF NOT EXISTS heart ('
                   'ui_id VARCHAR(256), address text, beat VARCHAR(32)'